BetaForms

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

slotwhat it is
DatePickerThe column — state, resolved style, one gap
DatePicker.LabelWhat the field is for. danger on isInvalid
DatePicker.FieldThe trigger in one tag — value and glyph inside it
DatePicker.TriggerThe control — the field the user sees
DatePicker.ValueThe chosen day, or the placeholder
DatePicker.IndicatorThe calendar glyph on the field
DatePicker.DescriptionThe hint under the field
DatePicker.ErrorWhat is wrong, in danger
DatePicker.OverlayThe backdrop. Optional
DatePicker.ContentThe panel
DatePicker.CalendarThe 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.

PropTypeRequiredDescription
variantDatePickerVariant | undefinedNo
sizeDatePickerSize | undefinedNo
radiusRadiusKey | undefinedNo
colorstring | undefinedNoThe tint (R7) — a raw value, never a token. The field, and the chosen day.
calendarVariantCalendarVariant | undefinedNoThe 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.
valueDate | undefinedNoThe chosen day. Controlled — leave it out and the picker holds its own.
defaultValueDate | undefinedNo
onValueChange((value: Date) => void) | undefinedNo
isOpenboolean | undefinedNoControlled open state.
defaultOpenboolean | undefinedNo
onOpenChange((isOpen: boolean) => void) | undefinedNo
minValueDate | undefinedNo
maxValueDate | undefinedNo
firstDayOfWeekWeekDay | undefinedNo
localestring | undefinedNo
formatOptionsDateTimeFormatOptions | undefinedNoHow the field reads the chosen day. Anything `Intl.DateTimeFormat` takes.
closeOnSelectboolean | undefinedNoWhether 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.
isDisabledboolean | undefinedNo
isInvalidboolean | undefinedNo
childrenReactNodeNo
asChildboolean | undefinedNo

Slots

DatePickerCalendarProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

This slot adds nothing to the props of its React Native node.

DatePickerContentProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

This slot adds nothing to the props of its React Native node.

DatePickerDescriptionProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo

DatePickerErrorProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo

DatePickerFieldProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
placeholderstring | undefinedNoThe line the field shows while no day is chosen.
childrenReactNodeNoReplaces 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.

PropTypeRequiredDescription
childrenReactNodeNo

DatePickerOverlayProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
isDismissableboolean | undefinedNo

DatePickerTriggerProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo
asChildboolean | undefinedNo

DatePickerValueProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo
placeholderstring | undefinedNo

Variants, sizes and colour

This component adds no visual axis of its own. The values it does take are in the generated types above and in the live demo.

Accessibility

  • The trigger is a button, not a combobox: 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.expanded follows the panel; aria-invalid follows isInvalid.
  • 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

This component declares no rule of its own. The migration guide covers variants, colours and slots.

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.