Field.DatePicker
Field.DatePicker is a button inside a Field that opens a Calendar in a Popover. Its value is a Temporal.PlainDate or null, and onValueChange gets the date picked in the calendar, which then closes. It takes the calendar's min, max, isDateDisabled, firstDayOfWeek and locale.
On this page
const id = useId();
const [date, setDate] = useState<Temporal.PlainDate | null>(null);
const [text, setText] = useState('');
const [invalid, setInvalid] = useState(false);
<Field.Group>
<Label.Root htmlFor={id}>Start date</Label.Root>
<Field.Root id={id} invalid={invalid}>
<Field.TextInput
value={text}
onChange={(event) => {
setText(event.target.value);
setDate(parseDate(event.target.value));
setInvalid(false);
}}
onBlur={() => {
if (date) setText(formatDate(date));
else setInvalid(text.trim() !== '');
}}
/>
<Field.DatePicker
value={date}
onValueChange={(next) => {
setDate(next);
setText(formatDate(next));
setInvalid(false);
}}
showValue={false}
/>
</Field.Root>
</Field.Group>
Here a Field.TextInput in the same field lets people type the date too. A picked date is written into the text with formatDate, and typed text that parseDate reads as a date moves the calendar to its month. Beside a Field.TextInput, that input is the field's control. It takes the field's id, name and description, and a press elsewhere in the field focuses it. With showValue={false}, the button shows only a calendar glyph. The button sends the date with a form under its own name if it has one, and otherwise under the field's name, unless a Field part or a control joined with useField, other than a Field.ColorPicker, Field.DatePicker, Field.DateRangePicker, Field.TimePicker or Field.DateTimePicker, has that name, as a Field.TextInput does when it has no name of its own. To send the date as 2026-10-13 rather than the typed text, put name on the picker instead of the Root.
Typed dates
formatDate(date, { locale }) writes a date in the locale's numbers, such as 10/13/2026 in en-US, 13.10.2026 in de and 2026. 10. 13. in ko. parseDate(text, { locale }) reads typed text back into a Temporal.PlainDate, or returns null when the text is not a date. It reads a date in numbers in the order formatDate writes them or in the order of the locale's short date style, as 13.10.26 in gsw, whose formatDate writes 2026-10-13. When both orders read a date, a year of four or more digits is read in formatDate's order, and a year of one or two digits in the short style's order when that style writes the year in two digits. It also reads a date as Intl.DateTimeFormat writes it in the locale's medium, long or full date style, such as Oct 13, 2026 or Tuesday, October 13, 2026 in en-US. Letter case doesn't matter, digits may be in any script, and the parts may be separated by any punctuation, symbols or spaces. The month may be any of its long or short names, such as Oct or October. A weekday may be long, short or left out, but must be the date's. The style's other words, such as de in 13 de octubre de 2026, may be replaced by a space. Three numbers that start with a year of four or more digits, such as 2026-10-13, are read as year, month and day, unless the locale writes the year first in another order. A year is a Gregorian year, whatever the locale's own calendar. A year of one or two digits is read as the nearest such year to the current one, so a date about 50 years away needs all four digits, and a year of three digits is not read. Without locale, both use the runtime's own.
In the example, text that isn't a date clears the date, so the form sends no date, and the field is marked invalid when it loses focus. parseDate doesn't check the picker's min, max or isDateDisabled. A typed date outside min and max still makes the form invalid, but check it against isDateDisabled yourself.
On its own
On its own, Field.DatePicker is the field's control. It shows the date in locale's medium style, or placeholder while there is none, and a press elsewhere in the field opens the calendar. With a name on the Root, the form sends the date the way HTML writes one, such as 2026-10-13.
The calendar opens and closes through open with onOpenChange, or defaultOpen. onOpenChange gets the reason it closed: 'select' when a date is picked, 'escape', 'outside', 'trigger', or 'disabled' when the field becomes disabled. Open, the calendar puts focus on the date, else on today or the nearest date in range. Escape closes it, and focus goes back to where it was before it opened.
required makes a form with no date invalid, and min and max make a form with an earlier or later date invalid, as well as limiting the calendar. The browser checks all three on a native <input type="date"> that the picker renders beside its button. It is hidden from sight and from assistive technology, is not a tab stop, and is what the form sends under the field's name. Inside a Form.Root, a problem shows in the field's ErrorMessage.Root in the page's language, with min or max written as the button writes a date, and focus goes to the button rather than the hidden input; the summary's row for the field links to the button. Outside a Form.Root the browser blocks the submit and focus lands on the button too. A native date input holds only the years 1 to 275760, so a value or bound outside them reads as no date: it is sent empty, required, min and max ignore it, and development builds warn once. isDateDisabled is not part of the check.
Accessible composition
Todae handles: On its own, the button takes id, aria-label, aria-labelledby and aria-describedby from Field.Root, and native disabled and aria-invalid from the Root or a surrounding Fieldset. Its label names it, so the date it shows is added to its aria-describedby. Beside a Field part or a control joined with useField, other than a Field.ColorPicker, a Field.DateRangePicker, a Field.TimePicker, a Field.DateTimePicker or another Field.DatePicker, such as a text input, that control takes the id and aria-describedby in its place, and the button keeps disabled and aria-invalid and is named from the datePicker string in StringsProvider unless it has its own aria-label. The button gets aria-haspopup="dialog" and aria-expanded, and aria-controls while the calendar is open. The popover is a role="dialog" named from the datePicker string, and the calendar inside is keyed as on Calendar. The glyph is calendar from GlyphsProvider. A required picker's button is described by the required string in StringsProvider, since a button cannot take aria-required.
You supply: A name, through a Label whose htmlFor is the Root id or aria-label / aria-labelledby on the Root. The same date for a text input and the picker beside it, written with formatDate and read with parseDate.
Watch for: A server render can't know another control sits beside the button, so until the page hydrates, the button and that control both carry the field's id, and both send a value under the field's name. With a value, the server writes that date in its own locale, so pass locale when the picker renders on a server.
When to use
Field.DatePicker
When: A date in a form where people benefit from seeing a month, such as a booking or a deadline.
When not: A date people know by heart, such as a birthday → a Field.TextInput read with parseDate. A calendar shown on the page → Calendar. A start and an end date → Field.DateRangePicker. A date with a time → Field.DateTimePicker.
formatDate
When: Writing a date as text for a locale, such as into a text input beside a picker.
When not: A date with a weekday or a month name → Intl.DateTimeFormat.
parseDate
When: Reading a date someone typed.
When not: Text in a fixed format such as 2026-10-13 → Temporal.PlainDate.from.
Props
| Name | Type | Description |
|---|---|---|
value Required | Temporal.PlainDate | null | The date, or null for none. |
onValueChange | (value: Temporal.PlainDate) => void | Fires with the date picked in the calendar. |
min | Temporal.PlainDate | The earliest date that can be picked. A form with an earlier date is invalid. |
max | Temporal.PlainDate | The latest date that can be picked. A form with a later date is invalid. |
required | boolean | A form with no date is invalid. |
isDateDisabled | (date: Temporal.PlainDate) => boolean | Returns true for a date that cannot be picked, such as a holiday. |
firstDayOfWeek | 1 | 2 | 3 | 4 | 5 | 6 | 7 | The calendar's first column, 1 (Monday) to 7 (Sunday). The locale's own first day when the browser reports it, else 7. |
locale | string | The locale for the button's date and the calendar's names, such as "ko". The browser's own when not set, so pass it when the picker renders on a server. |
placeholder | string | Shown on the button while there is no date, when showValue is true. |
showValue | boolean | Whether the button shows the date, or the placeholder, beside its glyph. Set it to false beside a text input that shows the date. Default: true |
open | boolean | Controlled open state of the calendar. Pass with onOpenChange. |
defaultOpen | boolean | Initial open state when uncontrolled. |
onOpenChange | (open: boolean, reason: 'escape' | 'outside' | 'trigger' | 'select' | 'disabled') => void | Fires when the calendar opens or closes, with why it closed. |
| Name | Type | Description |
|---|---|---|
date Required | Temporal.PlainDate | The date to write. |
options.locale | string | The locale, such as ko. The runtime's own when not set. |
| Name | Type | Description |
|---|---|---|
text Required | string | The typed text. |
options.locale | string | The locale, such as ko. The runtime's own when not set. |