DatePicker
A field that opens a month.
Overview
A field that opens a month.
First example
<DatePicker value={date} onValueChange={setDate} isInvalid={Boolean(error)}><DatePicker.Label>Date de naissance</DatePicker.Label><DatePicker.Field placeholder="Choisir une date" />{error ? (<DatePicker.Error>{error}</DatePicker.Error>) : (<DatePicker.Description>jj/mm/aaaa</DatePicker.Description>)}<DatePicker.Overlay /><DatePicker.Content><DatePicker.Calendar /></DatePicker.Content></DatePicker>
Anatomy
| slot | what it is |
|---|---|
DatePicker | The column — state, resolved style, one gap |
DatePicker.Label | What the field is for. danger on isInvalid |
DatePicker.Field | The trigger in one tag — value and glyph inside it |
DatePicker.Trigger | The control — the field the user sees |
DatePicker.Value | The chosen day, or the placeholder |
DatePicker.Indicator | The calendar glyph on the field |
DatePicker.Description | The hint under the field |
DatePicker.Error | What is wrong, in danger |
DatePicker.Overlay | The backdrop. Optional |
DatePicker.Content | The panel |
DatePicker.Calendar | The month, bound to the picker |
The root is the TextField's column, token for token: the same root gap, the same
label and help styles, resolved through textFieldRecipe. A date field and a text field
stacked in one form line up and read as one control — the same reason the trigger is a
Select's and the grid is a Calendar.
isInvalid paints the colours; the caller mounts the message. It turns the border, the
label and the description danger — it does not add or remove DatePicker.Error, because a
slot that silently renders nothing is one you cannot debug. You write the condition and see
it, exactly as on a TextField. No auto-wrap either (R3): a string child of the root is not
a label or a hint in any way the component could guess.
Usage
<DatePicker value={date} onValueChange={setDate} isInvalid={Boolean(error)}>
<DatePicker.Label>Date de naissance</DatePicker.Label>
<DatePicker.Field placeholder="Choisir une date" />
{error ? (
<DatePicker.Error>{error}</DatePicker.Error>
) : (
<DatePicker.Description>jj/mm/aaaa</DatePicker.Description>
)}
<DatePicker.Overlay />
<DatePicker.Content>
<DatePicker.Calendar />
</DatePicker.Content>
</DatePicker>
DatePicker.Field is the trigger in one tag — DatePicker.Trigger around
DatePicker.Value and DatePicker.Indicator. Open it up for finer control:
<DatePicker.Trigger>
<DatePicker.Value placeholder="Choisir une date" />
<DatePicker.Indicator />
</DatePicker.Trigger>
Root props
DatePickerProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| variant | DatePickerVariant | undefined | No | — |
| size | DatePickerSize | undefined | No | — |
| radius | RadiusKey | undefined | No | — |
| color | string | undefined | No | The tint (R7) — a raw value, never a token. The field, and the chosen day. |
| calendarVariant | CalendarVariant | undefined | No | The calendar's own emphasis, which is **not** the field's. A `ghost` field over a `primary` calendar is the ordinary case: the trigger is quiet on the form and the chosen day is not. |
| value | Date | undefined | No | The chosen day. Controlled — leave it out and the picker holds its own. |
| defaultValue | Date | undefined | No | — |
| onValueChange | ((value: Date) => void) | undefined | No | — |
| isOpen | boolean | undefined | No | Controlled open state. |
| defaultOpen | boolean | undefined | No | — |
| onOpenChange | ((isOpen: boolean) => void) | undefined | No | — |
| minValue | Date | undefined | No | — |
| maxValue | Date | undefined | No | — |
| firstDayOfWeek | WeekDay | undefined | No | — |
| locale | string | undefined | No | — |
| formatOptions | DateTimeFormatOptions | undefined | No | How the field reads the chosen day. Anything `Intl.DateTimeFormat` takes. |
| closeOnSelect | boolean | undefined | No | Whether choosing a day closes the panel. On by default: a picker whose only job is one date has been answered the moment a day is pressed, and a panel that stayed open would need a second control to say so. Off for a picker inside a form that confirms. |
| isDisabled | boolean | undefined | No | — |
| isInvalid | boolean | undefined | No | — |
| children | ReactNode | No | — |
| asChild | boolean | undefined | No | — |
Slots
DatePickerCalendarProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
DatePickerContentProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
DatePickerDescriptionProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
DatePickerErrorProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
DatePickerFieldProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| placeholder | string | undefined | No | The line the field shows while no day is chosen. |
| children | ReactNode | No | Replaces the value and the calendar glyph inside the trigger. |
DatePickerLabelProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
DatePickerOverlayProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| isDismissable | boolean | undefined | No | — |
DatePickerTriggerProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
| asChild | boolean | undefined | No | — |
DatePickerValueProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
| placeholder | string | undefined | No | — |
Variants, sizes and colour
Accessibility
- The trigger is a
button, not acombobox: there is nothing to type into, and a month grid is not a list of options a screen reader can walk from the field.accessibilityState.expandedfollows the panel;aria-invalidfollowsisInvalid. - The backdrop announces nothing — it is the absence of the panel, and "button" over the whole screen is worse than silence.
- Every day inside is labelled with its full date by the
Calendar.
Migration from legacy
Implementation notes
It owns almost nothing
The trigger is a Select's trigger, the panel is a Select's panel, and the grid
is a Calendar — all three by construction rather than by
resemblance. A select and a date field in one form cannot drift apart, and a calendar in a
picker cannot differ from one on a page.
What this component adds is the wiring, and it is worth naming because each piece is a place two things could otherwise disagree:
- the chosen day read into the field through
Intl, - a panel that closes when a day is pressed,
- one set of bounds, which the field, the grid and the chevrons all read.
DatePicker.Calendar takes the Calendar's props minus the ones the picker already owns —
value, bounds, locale, first day, variant, disabled — because two sources for one of them
would be two answers to one question.
The panel matches the field, with the grid as its floor
width defaults to trigger with a minWidth of seven cells — the Calendar's
own 7 × cell at the picker's size.
So a narrow field opens a panel exactly seven cells wide, and a wide field opens one as wide
as itself: the panel belongs to the field, and a month grid squeezed under a narrow one
would crush its columns or clip a week. DatePicker.Calendar fills the panel (width: 100%)
so a wide one spreads its cells rather than leaving a gap beside them.
Override it like any anchored surface: width="content-fit" for the compact seven-cell
panel whatever the field, width={320} for a fixed one, or your own minWidth.
minWidth is maxWidth's mirror — a floor under the resolved width, on DatePicker.Content
and Select.Content, and the screen edge still overrides it.
The field's level is not the calendar's
<DatePicker variant="ghost" calendarVariant="primary" />
A quiet trigger over an emphatic chosen day is the ordinary case, so variant dresses the
field and calendarVariant dresses the grid. color reaches both — the field's fill and the
chosen day's disc are the same brand.
The month on screen is not bound
The picker owns the value; the calendar owns the month it is showing. Opening the
panel a second time after paging leaves you where you were, and choosing a day in another
month still works — paging is not choosing, which calendar.md argues at length.
A year and a month, from the panel
DatePicker.Calendar is a Calendar, so its year → month → day walk composes here exactly
as it does on a page: pass the children, hold view, and mount Calendar.YearPicker /
Calendar.MonthPicker in the grid's place.
const [view, setView] = useState<CalendarView>('grid')
<DatePicker onOpenChange={open => { if (!open) setView('grid') }}>
{/* trigger, overlay … */}
<DatePicker.Content>
<DatePicker.Calendar view={view} onViewChange={setView}>
<Calendar.Header>
<Calendar.PreviousButton />
<PressableFeedback onPress={() => setView(v => (v === 'grid' ? 'year' : 'grid'))}>
<Calendar.Title />
</PressableFeedback>
<Calendar.NextButton />
</Calendar.Header>
{view === 'year' ? (
<Calendar.YearPicker />
) : view === 'month' ? (
<Calendar.MonthPicker />
) : (
<>
<Calendar.Weekdays />
<Calendar.Grid />
</>
)}
</DatePicker.Calendar>
</DatePicker.Content>
</DatePicker>
view is the Calendar's, not the picker's — resetting it to 'grid' on close is what
makes the panel open on the month every time.
`closeOnSelect`
On by default: a picker whose only job is one date has been answered the moment a day is pressed, and a panel that stayed open would need a second control to say so.
closeOnSelect={false} for a picker inside a form that confirms — where the caller writes
their own footer under the grid through DatePicker.Calendar's children.
`formatOptions`
<DatePicker formatOptions={{ dateStyle: 'full' }} />
Anything Intl.DateTimeFormat takes. dateStyle: 'medium' by default — "6 sept. 2026" —
because a field is a line on a form and the long form is a sentence.
It changes how the field reads the day and nothing else: the value is a Date at
midnight local time either way. A Hermes build with no ICU falls back to the ISO day, which
is unambiguous everywhere even where it is nobody's habit.