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
Dir
Presets
Modes
Props
Code
<Component
numOfMonths={1}
minuteStep={1}
timePlaceholder="--"
/>Single date
The default shape — a native Date | null value, exactly like Calendar's own.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 28 | 29 | 30 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
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.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 28 | 29 | 30 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
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.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 28 | 29 | 30 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 28 | 29 | 30 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
<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.
| L | M | M | J | V | S | D |
|---|---|---|---|---|---|---|
28 | 29 | 30 | 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 1 |
// 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.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 28 | 29 | 30 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
<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.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 28 | 29 | 30 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
<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.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 28 | 29 | 30 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
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.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 28 | 29 | 30 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
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.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 28 | 29 | 30 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
<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.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 28 | 29 | 30 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
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.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
27 | 28 | 29 | 30 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
1 | 2 | 3 | 4 | 5 | 6 | 7 |
<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>witharia-haspopup="grid"andaria-expanded, all from Ark. Undertypeable, 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
Calendarhas — arrow-key navigation,aria-disabledon 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 decorativeh/m/ssuffix shown in their place, not removed. - Cancel/Apply (under
withConfirm) are real<button>s with accessible names. - The portaled popover re-stamps
data-colorso the palette survives being rendered outside the trigger's own DOM subtree.
Changelog
- #331
A
translationsprop for the built-in trigger, clear, remove, search, increment/decrement and upload labels - #317
Calendar, DatePicker, DataTable's date filter and Schedule's event details follow
IntlProvider'stimeZone(prop, then provider, then host) instead of always reading the host zone - #327
When the time zone changes, the picked date keeps its instant and is shown in the new zone. Nothing remounts,
onChangedoesn't fire, and focus stays where it was - #329
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"
- #186
Date restriction predicates resolve in the component's own time zone
- #135
Opened inside a
ModalorDrawer, an overlay portals into it, so it always paints above its host
- #45
MultiSelect's
searchPlaceholder, DatePicker'stimePlaceholderand TimeInput'splaceholder
- #37
Borders and dividers on floating panels match the panel's own surface
Time Input
HOUR/MINUTE (+ SECOND) + AM/PM time entry composed from NumberInput + SegmentGroup — Ark ships no time-picker machine, so this is the kit's own composed time field.
Badge
Compact inline label. Five variants × eleven colors, sharing Button's accent system but tuned for status chips and counters.