# DatePicker

A date — or a span of them — chosen from a calendar.

- Group: Forms
- Import: `import { DatePicker } from '@misoto22/folio'`
- Page: https://ui.misoto22.com/components/date-picker/
- Related: calendar, field

## When to reach for it

Deliberately not a text input with a calendar attached: parsing a typed date needs a format, and 03/04 is March the fourth in one country and the third of April in the next. When the date is a long way back, the calendar’s month and year are dropdowns.

## Anatomy

- **Trigger** (required) — A <button> printing format(value) or the placeholder, with a calendar glyph pinned at the end. It is named by the label and the printed date together, so the format is heard as well as seen.
- **Panel** — A Popover holding the rail and the grid — side by side from sm up, stacked below it, where two months would not fit anyway.
- **Preset rail** — A role="group" of plain buttons, present only when presets is set: on by default for DateRangePicker, off by default for DatePicker. A shortcut that lands on a disabledDates day is drawn unavailable and refuses the click.
- **Calendar grid** — The shared Calendar, autoFocus on open so the keyboard lands in the month rather than back at the trigger. Two months at once on the range picker, from months.
- **Half-range text** — The range trigger prints “from – …” while only one end is chosen, so a half-answered range says so on the closed control instead of looking finished.

## Best practices

### Do

- Put the restriction in disabledDates rather than in your own handler. The rail asks it too, so a shortcut on a blocked day is disabled instead of committing a date the grid beside it refuses — a range preset is tested at its ENDS, so one straddling a blocked day is still offered, exactly as the grid still allows it.
- Pick controlled or uncontrolled and stay there. The current value is value ?? uncontrolled, so a controlled picker that clears by setting value to undefined falls through to whatever defaultValue seeded and the old date reappears.
- Validate a range before you use it: half a range is a legal state here — from set, to undefined — so a submit handler that reads value.to without checking gets undefined from a reader who simply closed the panel early.

### Don’t

- A Field’s required does not reach the trigger. It is a plain <button>, a role with nowhere to put aria-required, so the asterisk above is the whole of the marking and a screen reader meets an ordinary optional field.
- Do not reach for it for a birth date. There is no defaultMonth to pass: the panel always opens on the current month, so a date decades back begins with every reader in the month-and-year picker.
- Do not disable it to show a fixed date. disabled takes the trigger out of the tab order and blocks its pointer events, and the trigger is the only place the chosen date is printed at all.

## Accessibility

- The trigger prints the date in the visitor’s own locale, not a fixed dd/mm/yyyy, and announces it as part of its own name — so format reaches a screen reader too.
- DateRangePicker keeps the panel open until both ends are chosen — a range is not a value until it has a second date.
- The shortcut rail is plain buttons, not a menu: they set the same value the grid beside them sets, so they belong to one control and Tab in the same pass.
- Presets are computed on click, so “today” means today even on a tab left open overnight.

## Keyboard

- Enter / Space — Opens the calendar.
- Escape — Closes it without choosing.

## RANGE_PRESETS

Re-export of `[
  { label: 'Last 7 days', value: () => daysAgo(7) },
  { label: 'Last 30 days', value: () => daysAgo(30) },
  { label: 'Last 90 days', value: () => daysAgo(90) },
  { label: 'Last 12 months', value: () => daysAgo(365) },
  {
    label: 'Month to date',
    value: () => {
      const to = new Date()
      return { from: new Date(to.getFullYear(), to.getMonth(), 1), to }
    },
  },
  {
    label: 'Year to date',
    value: () => {
      const to = new Date()
      return { from: new Date(to.getFullYear(), 0, 1), to }
    },
  },
]`.

The shortcuts a range picker is asked for on nearly every screen it appears on, so they ship rather than being rebuilt per dashboard. Computed on click: a preset list built at render time freezes "today" at whenever the page loaded, which is wrong for anything left open overnight.

## DATE_PRESETS

Re-export of `[
  { label: 'Today', value: () => new Date() },
  {
    label: 'Tomorrow',
    value: () => {
      const date = new Date()
      date.setDate(date.getDate() + 1)
      return date
    },
  },
  {
    label: 'In a week',
    value: () => {
      const date = new Date()
      date.setDate(date.getDate() + 7)
      return date
    },
  },
  {
    label: 'In a month',
    value: () => {
      const date = new Date()
      date.setMonth(date.getMonth() + 1)
      return date
    },
  },
]`.

The single-date equivalent.

## DatePicker

A date, chosen from a calendar. A trigger and a `Calendar` in a `Popover` — not a new component so much as the composition people otherwise assemble slightly differently on every screen. It is deliberately NOT a text input with a calendar attached. A typed date needs parsing, and parsing needs a format, and a format is a locale argument nobody wins. When typing genuinely matters — a birth date, a long way back — the calendar's month and year are dropdowns, which is the same journey without the ambiguity.

### Props

- `value` — `Date`.
- `defaultValue` — `Date`.
- `onValueChange` — `(value: Date | undefined) => void`.
- `label` (required) — `string`. Names the control. Required — the trigger's text is a value, not a label. Announced together with the printed date rather than instead of it, so `format` reaches a screen reader as well as the screen. Inside a `Field` with a label, that label names the trigger and this one is not repeated.
- `placeholder` — `string` default `'Pick a date'`.
- `disabled` — `boolean` default `false`.
- `disabledDates` — `ComponentProps<typeof Calendar>['disabled']`. Days the reader may not choose. Reaches the calendar AND the shortcut rail: a preset landing on a blocked day is drawn unavailable and refuses the click, rather than committing a value the grid beside it would not accept.
- `format` — `(date: Date) => string` default `formatDate`. How the chosen date is printed on the trigger.
- `presets` — `boolean | DatePreset<Date>[]`. Shortcuts shown beside the grid. Pass `true` for the built-in set, an array for your own, or leave it off for none.
- `className` — `string`.
- `id` — `string`. The TRIGGER's id — the element a label points at. A `Field` sets it.
- `aria-describedby` — `string`. Ids of the copy describing the control. A `Field` sets it from hint, error and description.
- `aria-invalid` — `boolean | 'true' | 'false'`. Announced on the trigger. A `Field` sets it from `error`.

## DateRangePicker

A span of dates — a stay, a reporting period, a filter. Two months side by side, because a range that crosses a month boundary is the common case, and paging back and forth to see both ends is what makes a range picker tiring. They stack under `sm`, where two would not fit — the calendar's own `months` class already carries that, so there is nothing to override. The panel stays open until both ends are chosen: a range is not a value until it has a second date, and closing on the first one would mean re-opening to finish.

### Props

- `value` — `DateRange`.
- `defaultValue` — `DateRange`.
- `onValueChange` — `(value: DateRange | undefined) => void`.
- `label` (required) — `string`. Names the control. Announced together with the printed range, not instead of it.
- `placeholder` — `string` default `'Pick a range'`.
- `disabled` — `boolean` default `false`.
- `disabledDates` — `ComponentProps<typeof Calendar>['disabled']`. Days the reader may not choose. Reaches the shortcut rail as well as the grid, at the ENDS of each preset range — a shortcut whose interior straddles a blocked day is still offered, the way the grid still lets a reader drag a range across one.
- `months` — `number` default `2`. How many months are shown side by side. Falls back to one under `sm`.
- `format` — `(date: Date) => string` default `formatDate`.
- `presets` — `boolean | DatePreset<DateRange>[]` default `true`. Shortcuts shown beside the grid — Last 30 days and its neighbours. `true` for the built-in set, an array for your own. On by default here and off on the single picker, because "last 30 days" is most of what a range picker is ever asked for, while a single date is usually a specific one.
- `className` — `string`.
- `id` — `string`. The TRIGGER's id — the element a label points at. A `Field` sets it.
- `aria-describedby` — `string`. Ids of the copy describing the control. A `Field` sets it from hint, error and description.
- `aria-invalid` — `boolean | 'true' | 'false'`. Announced on the trigger. A `Field` sets it from `error`.

## Example — default

```tsx
import { DatePicker, Field } from '@misoto22/folio'

<Field
  label="Publish on"
  hint="Month and year are dropdowns — reaching two years back is one click, not twenty-four."
  className="w-full max-w-xs"
>
  <DatePicker label="Publish on" />
</Field>
```

## Example — range

```tsx
import { DateRangePicker, Field } from '@misoto22/folio'

<Field
  label="Reporting period"
  hint="Last 30 days and its neighbours are one click; the grid is for everything else."
  className="w-full max-w-sm"
>
  <DateRangePicker label="Reporting period" />
</Field>
```

## Example — presets

```tsx
import { DatePicker, Field } from '@misoto22/folio'

<Field
  label="Remind me on"
  hint="Shortcuts are computed when clicked, so “today” means today even on a tab left open overnight."
  className="w-full max-w-xs"
>
  <DatePicker label="Remind me on" presets />
</Field>
```

## Example — blocked dates

```tsx
import { DatePicker, Field } from '@misoto22/folio'

<Field
  label="Site visit"
  hint="Weekdays only, and not in the past."
  className="w-full max-w-xs"
>
  <DatePicker label="Site visit" disabledDates={[{ before: TODAY }, { dayOfWeek: [0, 6] }]} />
</Field>
```
