# DatePicker (/docs/date-picker)





`DatePicker` is the flagship of the dates & time family — a styled trigger that opens a popover
calendar, composing [`Calendar`](/docs/calendar)'s shared grid module and
[`TimeInput`](/docs/time-input)'s shared time row. `range` and `withTime` cover all four date
combinations; `DateRangePicker`, `DateTimePicker`, and `DateTimeRangePicker` are thin named
presets pinning those two flags with a narrowed value type.

```tsx
import { DatePicker } from '@42-central/ui-react/date-picker';

<DatePicker label="Start date" value={date} onChange={setDate} clearable />
```

## Playground [#playground]

<story.Playground />

## Single date [#single-date]

The default shape — a native `Date | null` value, exactly like [`Calendar`](/docs/calendar)'s own.

<story.Preview variant="Default" />

```tsx
import { useState } from 'react';
import { DatePicker } from '@42-central/ui-react/date-picker';

const [date, setDate] = useState<Date | null>(null);

<DatePicker value={date} onChange={setDate} />
```

## Range [#range]

`range` selects a `{ start, end }` pair instead of a single date — click once for the start,
again for the end.

<story.Preview variant="Range" />

```tsx
import { useState } from 'react';
import { DateRangePicker, type DateRangeValue } from '@42-central/ui-react/date-picker';

const [range, setRange] = useState<DateRangeValue>({ start: null, end: null });

<DateRangePicker value={range} onChange={setRange} />
```

## With time [#with-time]

`withTime` adds an embedded HOUR/MINUTE row (`TimeInput`'s own `TimeRow`) below the calendar
grid; the value carries the picked hour/minute on the same `Date`. Combine with `range` for all
four combinations. Setting a time before picking a day selects today (kept within `min`/`max`),
so the edit is never lost.

<story.Preview variant="With time" />

<story.Preview variant="Range + time" />

```tsx
<DateTimePicker value={date} onChange={setDate} />
<DateTimeRangePicker value={range} onChange={setRange} />
```

12h vs 24h follows the active locale by default (same rule as `TimeInput`) — override with
`format={12 | 24}` or the finer `hourCycle` escape hatch.

<story.Preview variant="Locale (fr-FR)" />

```tsx
// French defaults to 24h — no AM/PM segment — and formats the trigger's date/time accordingly.
<DateTimePicker locale="fr-FR" value={date} onChange={setDate} />
```

## Typeable [#typeable]

`typeable` swaps the plain button-only trigger for Ark's own `Input` anatomy part — a real
`<input>` you can type a date into, alongside a calendar-icon button that still opens the popover.
Typing validates characters against the locale's date separator as you go, blurring (or `Enter`)
parses and commits the value, and an invalid or partial value on blur is discarded rather than
crashing (`fixOnBlur`, on by default). Clicking into the field opens the calendar too, not just
the icon button — so typing and picking both work off the same control.

<story.Preview variant="Typeable" />

```tsx
<DatePicker typeable value={date} onChange={setDate} />
```

### With time [#with-time-1]

Under `withTime` the field carries the time as well, as a numeric date + time in the locale's own
field order — `10/20/2026, 02:30 PM` in `en-US`, `20/10/2026 14:30` in `fr-FR` — so what it shows
is exactly what you can type back. Any separator works between the numbers (`20.10.2026 14h30`),
typing just a date keeps the current time, and a 12h locale reads `am`/`pm` (AM when omitted).
Text that isn't a real date/time reverts on blur. The popover's time row and the field stay in
sync: edit either one and the other follows.

<story.Preview variant="Typeable + time" />

```tsx
<DateTimePicker typeable value={date} onChange={setDate} />
```

Opt-in — default `false` keeps today's click-to-open button unchanged, so this is a purely
additive prop. It only takes effect for a single date: `range` falls back to the button trigger,
since cross-field validation across two inputs isn't wired up. Typing on
`DateRangePicker`/`DateTimeRangePicker` and on
[`DataTable`'s date filter](#using-it-as-a-datatable-date-filter) are both deferred to a follow-up
once this single-date pattern is validated.

## Presets [#presets]

`presets` renders a sidebar of quick-pick shortcuts. `presets={true}` ships a mode-appropriate
default set; pass an array for a fully custom list. `DatePreset` has three shapes: `{ value }`
(one of Ark's own range preset strings), `{ range }` (an explicit `{ start, end } | null`, range
mode), or `{ date }` (a single `Date | null`, single-date mode) — nothing stops mixing shapes for
the current mode, so keep custom entries matched to `range`.

### Range presets [#range-presets]

Default set: Today, Yesterday, This week, Last week, This month, Last month, This year, All time.

<story.Preview variant="Presets" />

```tsx
import { DateRangePicker, type DatePreset } from '@42-central/ui-react/date-picker';

// Default set
<DateRangePicker presets value={range} onChange={setRange} />

// Custom — mix Ark's own string presets with explicit ranges; `range: null` clears/unbounds.
const customPresets: DatePreset[] = [
  { label: 'This week', value: 'thisWeek' },
  { label: 'This year', value: 'thisYear' },
  { label: 'All time', range: null },
];

<DateRangePicker presets={customPresets} value={range} onChange={setRange} />
```

### Single-date presets [#single-date-presets]

Default set: Today, Yesterday, Tomorrow, Next week — a different, forward-facing shape than the
range set (a due date or appointment, not an analytics window), not a trimmed-down copy of it —
"This week"/"This month" are inherently range-shaped and don't collapse to one date.

<story.Preview variant="Presets (single)" />

```tsx
import { DatePicker, type DatePreset } from '@42-central/ui-react/date-picker';

// Default set
<DatePicker presets value={date} onChange={setDate} />

// Custom — `date: null` clears.
const customPresets: DatePreset[] = [
  { label: 'Next Friday', date: nextFriday },
  { label: 'Clear', date: null },
];

<DatePicker presets={customPresets} value={date} onChange={setDate} />
```

## Staged commit (withConfirm) [#staged-commit-withconfirm]

Without `withConfirm`, a single date commits (and closes) on pick, and a range commits on
its second click — both handled natively by Ark. Under `withTime` a pick still commits but the
popover stays open, since the time row below the calendar is the other half of the value —
click outside or press `Escape` to close it. With `withConfirm`, the popover stays open
after a pick: Cancel/Apply buttons appear, and only Apply calls the outer `onChange` — Cancel
reverts to the last committed value.

<story.Preview variant="Staged commit" />

```tsx
<DatePicker withConfirm value={date} onChange={setDate} />
```

## Translations [#translations]

`locale` drives month/day names, first-day-of-week, and the embedded time row's 12h/24h default
automatically, but everything else in this popover — Ark's own accessible names, the
`presets={true}` default labels, Cancel/Apply, and Start/End time — is English by default. The
time row's group is named "Start time" / "End time" under `range`, and "Time" (`time`, screen-reader
only) for a single date.
`translations` is one prop covering all of it; it's partial, so unset keys keep their English text.

```tsx
<DateRangePicker
  locale="fr-FR"
  presets
  withConfirm
  translations={{
    presets: { today: "Aujourd'hui", thisWeek: 'Cette semaine' },
    cancel: 'Annuler',
    apply: 'Valider',
    startTime: 'Début',
    endTime: 'Fin',
  }}
  value={range}
  onChange={setRange}
/>
```

## Restricting selection [#restricting-selection]

`isDateUnavailable`, `min`, and `max` work exactly as they do on [`Calendar`](/docs/calendar) —
see its [Restricting selection](/docs/calendar#restricting-selection) section for the full set
of composable predicate helpers (`disablePast()`, `disableWeekends()`, `beforeDate(d)`, `anyOf(...)`,
…), re-exported from this same `@42-central/ui-react/date-picker` barrel, and for the `timeZone` a
predicate is handed as its second argument.

<story.Preview variant="Restricted" />

```tsx
import { DatePicker, disablePast, disableWeekends, anyOf } from '@42-central/ui-react/date-picker';

<DatePicker isDateUnavailable={anyOf(disablePast(), disableWeekends())} />
```

## Fixed height [#fixed-height]

Same as [`Calendar`](/docs/calendar#fixed-height) — `fixedWeeks` always renders 6 week rows
(padding with the adjacent month's days) instead of however many the visible month(s) actually
span (4–6), keeping the popover's height constant while paging between months.

<story.Preview variant="Fixed weeks" />

```tsx
<DatePicker fixedWeeks />
```

## Aliases [#aliases]

Named presets pinning `range`/`withTime` — the same component, a narrowed value type at the call
site. See [`DatePicker`'s full API](#api) below; nothing about them is component-specific beyond
which flags they pin.

* **`DateRangePicker`** === `<DatePicker range />`
* **`DateTimePicker`** === `<DatePicker withTime />`
* **`DateTimeRangePicker`** === `<DatePicker range withTime />`

## Forms [#forms]

`name` wires a hidden `<input type="hidden">` into native form submission — `${name}Start` /
`${name}End` under `range` — serialized as a plain `YYYY-MM-DD` (or `YYYY-MM-DDTHH:mm:ss` under
`withTime`), read directly off the machine's own value.

```tsx
<DatePicker name="dueDate" />
<DateRangePicker name="period" />
```

## Using it as a DataTable date filter [#using-it-as-a-datatable-date-filter]

[`DataTable`](/docs/data-table)'s `date` / `date-range` column filters (`meta.filter`) render this
exact popover content — the calendar grid, presets rail, and (under `withTime`) time row —
composed directly from `DatePicker`'s exported parts, not a separate implementation:

```tsx
// data-table-filter/data-table-date-filter.tsx (simplified)
<DatePicker.Root
  inline // the header cell's own Popover already owns the trigger/visibility
  selectionMode="range" // both `date` and `date-range` are range-shaped UIs — see below
  locale={locale}
  timeZone={timeZone}
  value={values} // parsed straight from the stored string bounds, see below
  onValueChange={(details) => commit(details.value)}
>
  <DatePicker.Context>
    {(api) => (
      <DatePicker.Content>
        {presets && <DatePicker.Presets presets={presets} range timeZone={timeZone} />}
        <DatePicker.Calendar numOfMonths={2} size="sm" timeZone={timeZone} />
        {withTime && (
          <DatePicker.TimePanelRow
            value={api.value[0]}
            onSetTime={(time) => api.setTime(time, 0)}
            hourCycle={hourCycle}
          />
        )}
      </DatePicker.Content>
    )}
  </DatePicker.Context>
</DatePicker.Root>
```

`.Context` (Ark's own context accessor) and `.TimePanelRow` (the bridge between `TimeRow`'s
display-space props and `api.setTime`/`api.value`, distinct from the dumber `.TimePanel` =
`TimeRow`) are exported from this module specifically for this composition. The bridge stays
entirely in Ark's own `DateValue` space — the filter's stored `"YYYY-MM-DD"` /
`"YYYY-MM-DDTHH:mm:ss"` strings convert via `@internationalized/date`'s `parseDate`/`parseDateTime`
inbound and this module's own exported `serializeDateValue` outbound, never round-tripping through
a native `Date`.

A calendar's range clicks are *ordered* (first click is the start, second the end) — a lone click
derives `{ op: "gte" }`; unlike the column's old two-independent-inputs UI, there's no interaction
that produces an upper-bound-only `{ op: "lte" }` value through this popover (the operator itself
is unaffected and still fully settable programmatically via controlled `columnFilters`). See
[`DataTable`'s own Date and date-range section](/docs/data-table#date-and-date-range) for the full
column-facing API, including the optional `presets` field.

## API [#api]

<TypeTable
  type="{
  range: { type: 'boolean', default: 'false', description: 'Select a { start, end } range instead of a single date.' },
  withTime: { type: 'boolean', default: 'false', description: 'Adds an embedded HOUR/MINUTE (+ SECOND) time row.' },
  value: { type: 'Date | null | { start, end }', description: 'Controlled value.' },
  defaultValue: { type: 'Date | null | { start, end }', description: 'Uncontrolled initial value.' },
  onChange: { type: '(value) => void', description: 'Fires with the next value.' },
  numOfMonths: { type: 'number', default: '1', description: 'Months shown side by side, regardless of range.' },
  fixedWeeks: { type: 'boolean', default: 'false', description: 'Always render 6 week rows, keeping the popover height constant.' },
  presets: { type: 'DatePreset[] | boolean', description: 'Preset sidebar. true = mode-appropriate default set.' },
  withConfirm: { type: 'boolean', default: 'false', description: 'Stage the selection; commit only on Apply.' },
  clearable: { type: 'boolean', default: 'false', description: 'Renders a clear trigger once a value is set.' },
  isDateUnavailable: { type: '(date: Date, timeZone?: string) => boolean', description: &#x22;Restriction predicate over a native Date, called with the component's own resolved timeZone.&#x22; },
  typeable: { type: 'boolean', default: 'false', description: &#x22;Renders Ark's Input anatomy part (typing, blur-to-parse) alongside the calendar-icon trigger. Single, non-withTime dates only — see Typeable above.&#x22; },
  min: { type: 'Date', description: 'Earliest selectable date.' },
  max: { type: 'Date', description: 'Latest selectable date.' },
  minView: { type: '&#x22;day&#x22; | &#x22;month&#x22; | &#x22;year&#x22;', default: '&#x22;day&#x22;', description: 'Lowest view level reachable via navigation.' },
  maxView: { type: '&#x22;day&#x22; | &#x22;month&#x22; | &#x22;year&#x22;', default: '&#x22;year&#x22;', description: 'Highest view level reachable via navigation.' },
  locale: { type: 'string', description: 'Overrides the active locale context.' },
  timeZone: { type: 'string', description: &#x22;Overrides the IntlProvider time zone for the Date <-> DateValue boundary (default: the browser's own).&#x22; },
  format: { type: '12 | 24', description: 'Force 12h/24h display for the embedded time row, overriding the locale default.' },
  hourCycle: { type: '&#x22;h11&#x22; | &#x22;h12&#x22; | &#x22;h23&#x22; | &#x22;h24&#x22;', description: 'Force a specific hour cycle, overriding format and the locale default.' },
  minuteStep: { type: 'number', default: '1', description: 'Minute stepper increment.' },
  withSeconds: { type: 'boolean', default: 'false', description: 'Adds a SECOND field to the time row.' },
  timePlaceholder: { type: 'string', default: '&#x22;--&#x22;', description: &#x22;Placeholder shown in the embedded time row's empty Hour/Minute/Second fields.&#x22; },
  translations: { type: 'DatePickerTranslations', description: &#x22;Overrides English strings — Ark's own accessible names, presets={true} labels, Cancel/Apply, Start/End time, the time row's Time/Hour/Minute/Second/Period labels and its steppers' incrementLabel / decrementLabel.&#x22; },
  placeholder: { type: 'string', description: 'Placeholder shown while no date is selected.' },
  startSlot: { type: 'ReactNode', description: 'Inline-start adornment. Defaults to a calendar icon.' },
  size: { type: '&#x22;xs&#x22; | &#x22;sm&#x22; | &#x22;md&#x22; | &#x22;lg&#x22; | &#x22;xl&#x22;', default: '&#x22;md&#x22;', description: 'Trigger + popover density.' },
  variant: { type: '&#x22;default&#x22; | &#x22;unstyled&#x22;', default: '&#x22;default&#x22;', description: 'Trigger shell surface.' },
  color: { type: 'string', description: 'Accent palette for the focus ring and popover.' },
  disabled: { type: 'boolean', default: 'false', description: 'Disable the whole picker.' },
  readOnly: { type: 'boolean', default: 'false', description: 'Full contrast, but rejects selection.' },
  invalid: { type: 'boolean', default: 'false', description: 'Red state.' },
  name: { type: 'string', description: 'Name(s) for native form submission — see Forms above.' },
  classNames: { type: '{ field?, control?, content?, presets?, calendar?, timePanel?, footer? }', description: 'Per-part class overrides.' },
  testId: { type: 'string', description: 'data-testid on the visible trigger button — the same shell-wrapped-control pattern as Select. See the Testing guide.' },
}"
/>

`DateRangePicker`, `DateTimePicker`, and `DateTimeRangePicker` are thin presets over the same
component (see [Aliases](#aliases) above), so `testId` — like every other prop — passes straight
through to the underlying `DatePicker` trigger with no extra wiring needed. See the
[Testing](/docs/testing) guide for the full `testId` convention across the kit.

## Accessibility [#accessibility]

* The trigger is a real `<button>` with `aria-haspopup="grid"` and `aria-expanded`, all from Ark.
  Under `typeable`, that button becomes an icon-only "open calendar" control (still with an
  Ark-supplied accessible name) and the value itself lives in a real `<input>` beside it.
* The popover grid inherits every keyboard/ARIA behavior [`Calendar`](/docs/calendar#accessibility)
  has — arrow-key navigation, `aria-disabled` on unavailable days, `aria-selected`.
* The embedded time row's HOUR/MINUTE fields and AM/PM toggle carry real accessible names, same
  as standalone [`TimeInput`](/docs/time-input#accessibility) — visually hidden (`sr-only`) with
  a decorative `h`/`m`/`s` suffix shown in their place, not removed.
* Cancel/Apply (under `withConfirm`) are real `<button>`s with accessible names.
* The portaled popover re-stamps `data-color` so the palette survives being rendered outside the
  trigger's own DOM subtree.
