Field.DateTimePicker

Field.DateTimePicker is a button inside a Field that opens a Calendar and a list of times in a Popover, side by side, or the list under the calendar on a narrow screen. Its value is a Temporal.PlainDateTime or null. onValueChange gets the date and time each time a pick leaves a date the calendar offers and a time in range for it. Picking a time while the calendar holds a date it offers closes the popover. It works like Field.DatePicker and Field.TimePicker, and takes the same props where they mean the same thing. A PlainDateTime has no time zone: it is the date and time on the user's clock. To store an instant, combine it with a time zone in your own code, such as value.toZonedDateTime(zone).

On this page
const id = useId();
const [start, setStart] = useState<Temporal.PlainDateTime | null>(null);
const [text, setText] = useState('');
const [invalid, setInvalid] = useState(false);

<Field.Group>
  <Label.Root htmlFor={id}>Meeting starts</Label.Root>
  <Field.Root id={id} invalid={invalid}>
    <Field.TextInput
      value={text}
      onChange={(event) => {
        setText(event.target.value);
        setStart(parseDateTime(event.target.value));
        setInvalid(false);
      }}
      onBlur={() => {
        if (start) setText(formatDateTime(start));
        else setInvalid(text.trim() !== '');
      }}
    />
    <Field.DateTimePicker
      value={start}
      onValueChange={(next) => {
        setStart(next);
        setText(formatDateTime(next));
        setInvalid(false);
      }}
      step={15}
      showValue={false}
    />
  </Field.Root>
</Field.Group>

Here a Field.TextInput in the same field lets people type the date and time too. A picked date and time is written into the text with formatDateTime. 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 and time 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 another Field.DateTimePicker, has that name, as a Field.TextInput does when it has no name of its own. To send the date and time as 2026-10-13T09:30 rather than the typed text, put name on the picker instead of the Root.

Typed dates and times

formatDateTime(value, { locale }) writes the date in numbers, as formatDate does, and the time in hours and minutes, or with seconds when it has seconds, such as 10/13/2026, 9:30 PM in en-US, 13.10.2026, 21:30 in de and 2026. 10. 13. 오후 9:30 in ko. parseDateTime(text, { locale }) reads typed text back into a Temporal.PlainDateTime, or returns null when the text is not one. It reads a date as parseDate does, with its year in full or as its last two digits, then a time as parseTime does, in the order the locale writes them, with or without the words the locale writes between them, such as Oct 13, 2026 at 9:30 PM or 10/13/2026 9:30 pm in en-US. The time needs its minutes, as in 21:30 or 2130, or a word, as in 9 pm, so an hour alone, as in 13.10.2026 21, is not read, and neither is a date without a time. Text that also reads as a month and day without a year and a time, as 10/13 12 05 pm in en-US, is not read. Without locale, both use the runtime's own.

In the example, text that isn't a date and time clears the value, so the form sends none, and the field is marked invalid when it loses focus. parseDateTime doesn't check the picker's min, max or isDateDisabled. A typed value outside min and max still makes the form invalid, but check it against isDateDisabled yourself.

On its own

On its own, Field.DateTimePicker is the field's control. It shows the date in the locale's medium date style and the time in its short time style, or its medium style when the time has seconds, or placeholder while there is no value, and a press elsewhere in the field opens the popover. With a name on the Root, the form sends the value the way HTML writes one, such as 2026-10-13T09:30.

The popover opens and closes through open with onOpenChange, or defaultOpen. onOpenChange gets the reason it closed: 'select' when a picked time makes the value, 'escape', 'outside', 'trigger', or 'disabled' when the field becomes disabled. Open, the calendar takes focus on the date, or else today or the nearest date in range. Picking a date moves focus to the list of times, and picking a time before any date moves it to the calendar. Escape closes the popover, and focus goes back to where it was before it opened.

min and max limit both halves: the calendar offers only the dates between them, and on the first and last of those dates the list offers only the times in range. A date picked with a time out of range for it isn't reported until a time in range is picked. A date the calendar doesn't offer, such as one typed beside the picker, counts as no date: picking a time then moves focus to the calendar. isDateDisabled and firstDayOfWeek work as on Field.DatePicker, and step as on Field.TimePicker.

required makes a form with no date and time invalid, and min and max make a form with an earlier or later one invalid, as well as limiting the calendar and the list. The browser checks all three on a native <input type="datetime-local"> 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 time, 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.

Temporal

Todae ships no Temporal polyfill. The picker's values, formatDateTime and parseDateTime are Temporal.PlainDateTime, so where the browser has no Temporal, the app loads a polyfill (see Native platform). Todae's types need TypeScript 6 or later, which declares Temporal.

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 and time it shows are added to its aria-describedby. Beside a Field part or a control joined with useField, other than a Field.ColorPicker, Field.DatePicker, Field.DateRangePicker, Field.TimePicker or another Field.DateTimePicker, 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 dateTimePicker string in StringsProvider unless it has its own aria-label. The button gets aria-haspopup="dialog" and aria-expanded, and aria-controls while the popover is open. The popover is a role="dialog" named from the dateTimePicker string. It holds a Calendar, with the keyboard described on its page, and an OptionList of times named from the timePicker string, where arrow keys, Home and End move and Enter or Space picks. 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 value for a text input and the picker beside it, written with formatDateTime and read with parseDateTime.

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 the date and time in its own locale, so pass locale when the picker renders on a server.

When to use

Field.DateTimePicker

When: A date and a time of day in one value, such as when a meeting starts or a flight departs.

When not: A date alone → Field.DatePicker. A time alone → Field.TimePicker. An instant in a time zone → this picker, with the zone added in your own code.

formatDateTime

When: Writing a date and time as text for a locale, such as into a text input beside a picker.

When not: A date and time with a weekday or a month name → Intl.DateTimeFormat.

parseDateTime

When: Reading a date and time someone typed.

When not: Text in a fixed format such as 2026-10-13T09:30 → Temporal.PlainDateTime.from.

Props

Field.DateTimePicker props
NameTypeDescription
value RequiredTemporal.PlainDateTime | null

The date and time, or null for none.

onValueChange(value: Temporal.PlainDateTime) => void

Fires with the date and time when a pick leaves a date the calendar offers and a time in range for it.

minTemporal.PlainDateTime

The earliest date and time that can be picked. A form with an earlier one is invalid.

maxTemporal.PlainDateTime

The latest date and time that can be picked. A form with a later one is invalid.

requiredboolean

A form with no date and time is invalid.

isDateDisabled(date: Temporal.PlainDate) => boolean

Returns true for a date that cannot be picked, such as a holiday.

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

stepnumber

Minutes between the times in the list, from midnight.

Default: 30

localestring

The locale for the button's text, the calendar's names and the times, such as "ko". The browser's own when not set, so pass it when the picker renders on a server.

placeholderstring

Shown on the button while there is no date and time, when showValue is true.

showValueboolean

Whether the button shows the date and time, or the placeholder, beside its glyph. Set it to false beside a text input that shows them.

Default: true

openboolean

Controlled open state of the popover. Pass with onOpenChange.

defaultOpenboolean

Initial open state when uncontrolled.

onOpenChange(open: boolean, reason: 'escape' | 'outside' | 'trigger' | 'select' | 'disabled') => void

Fires when the popover opens or closes, with why it closed.

formatDateTime props
NameTypeDescription
value RequiredTemporal.PlainDateTime

The date and time to write.

options.localestring

The locale, such as ko. The runtime's own when not set.

parseDateTime props
NameTypeDescription
text Requiredstring

The typed text.

options.localestring

The locale, such as ko. The runtime's own when not set.