|Kit

DatePicker

A trigger + popover calendar on Ark UI's date-picker machine, composing Calendar's grid and TimeInput's time row — range and withTime cover all four date combinations.

DatePicker is the flagship of the dates & time family — a styled trigger that opens a popover calendar, composing Calendar's shared grid module and TimeInput'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.

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

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

Playground

Component

Loading preview

Modes

Props

Code

<Component
  numOfMonths={1}
  minuteStep={1}
  timePlaceholder="--"
/>

Single date

The default shape — a native Date | null value, exactly like Calendar's own.

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 selects a { start, end } pair instead of a single date — click once for the start, again for the end.

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

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.

<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.

// 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 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.

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

With time

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.

<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 are both deferred to a follow-up once this single-date pattern is validated.

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

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

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

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.

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)

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.

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

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.

<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

isDateUnavailable, min, and max work exactly as they do on Calendar — see its 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.

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

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

Fixed height

Same as Calendar — 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.

<DatePicker fixedWeeks />

Aliases

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

  • DateRangePicker === <DatePicker range />
  • DateTimePicker === <DatePicker withTime />
  • DateTimeRangePicker === <DatePicker range withTime />

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.

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

Using it as a DataTable date filter

DataTable'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:

// 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 for the full column-facing API, including the optional presets field.

API

Prop

Type

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

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 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 — 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.

Changelog

v0.19.0
  • A translations prop for the built-in trigger, clear, remove, search, increment/decrement and upload labels

    #331
  • Calendar, DatePicker, DataTable's date filter and Schedule's event details follow IntlProvider's timeZone (prop, then provider, then host) instead of always reading the host zone

    #317
  • When the time zone changes, the picked date keeps its instant and is shown in the new zone. Nothing remounts, onChange doesn't fire, and focus stays where it was

    #327
  • Time-segment and AM/PM groups take their name from the field label, and DatePicker's range rows are announced as "Start time" and "End time"

    #329
v0.16.0
  • DatePicker's typeable works with withTime, and TimeInput moves between fields with the arrow keys

    #250
  • An outside click no longer dismisses the parent while a nested overlay is open

    #251
v0.12.0
  • Date restriction predicates resolve in the component's own time zone

    #186
v0.9.0
  • Opened inside a Modal or Drawer, an overlay portals into it, so it always paints above its host

    #135
v0.6.0
  • Inputs drop the filled variant and the color prop

    #96
  • typeable lets a single date be typed into the field

    #105
v0.4.0
  • MultiSelect's searchPlaceholder, DatePicker's timePlaceholder and TimeInput's placeholder

    #45
v0.3.1
  • Borders and dividers on floating panels match the panel's own surface

    #37
v0.3.0
  • translations overrides the built-in English microcopy, including in DataTable's date filters

    #24
  • The withTime popover hugs the calendar

    #23
v0.2.0
  • New DatePicker: a trigger and popover over the Calendar grid, with ranges, withTime and a presets sidebar

    #18
  • fixedWeeks keeps the grid at the same height every month

    #18

On this page