LocalDateTimeField
A date and a time as one wall-clock value without a zone, with the zone it is read in shown beside it.
Use Local for a moment that a person writes as a date and a clock time: a planned
send, the start of a meeting. It combines a DateField and a
TimeField. The value is a wall clock, YYYY-, with no
zone: turning it into an instant is your application's work.
Value: 2026-11-02T09:00 · state: valid
// apps/docs/examples/ui/local-date-time-field.tsx'use client';
import {useState} from 'react';
import { composePresentationContext, toIanaTimeZone, toLocalDateTimeString,} from '@workspace/active-format';import { LocalDateTimeField, type LocalDateTimeFieldState,} from '@workspace/active-ui/components/composed/local-date-time-field';import { Field, FieldDescription, FieldError, FieldLabel,} from '@workspace/active-ui/components/primitives/field';
const TODAY = '2026-10-20';const TOMORROW = toLocalDateTimeString('2026-10-21T00:00');const PRESENTATION = composePresentationContext( 'en', {}, toIanaTimeZone('Europe/Amsterdam'),);
/** * A date and a time together, as a wall-clock value without a zone * (`YYYY-MM-DDTHH:mm`). The zone it is read in is shown beside it; turning * it into a moment is the application's work. A date without a time is * incomplete and never travels out as a value. */export default function LocalDateTimeFieldExample() { const [value, setValue] = useState('2026-11-02T09:00'); const [state, setState] = useState<LocalDateTimeFieldState>({ status: 'valid', }); const invalid = state.status === 'invalid'; return ( <Field className="w-full max-w-sm" data-invalid={invalid || undefined}> <FieldLabel id="send-label" htmlFor="send-at"> Send the statements at </FieldLabel> <LocalDateTimeField id="send-at" aria-labelledby="send-label" aria-describedby="send-at-state" value={value} onValueChange={setValue} onStateChange={setState} locale={PRESENTATION} today={TODAY} min={TOMORROW} step={{unit: 'minute', value: 15}} openCalendarLabel="Choose a date" dateLabel="Date" timeLabel="Time" timeZoneLabel="Europe/Amsterdam" aria-invalid={invalid || undefined} /> {invalid ? ( <FieldError id="send-at-state"> {state.rejection === 'range' ? 'Choose a moment from tomorrow on.' : state.rejection === 'step' ? 'Choose a quarter of an hour.' : 'Write a date and a time.'} </FieldError> ) : ( <FieldDescription id="send-at-state"> Value: <code>{value || "''"}</code> · state: {state.status} </FieldDescription> )} </Field> );}What the composition adds#
One value for two parts. Only a complete, valid date and time is a value; clearing both parts gives
''. A date without a time isincompleteand never travels out.One state for the pair, in the shape of
Time's:Field empty,incomplete,valid, orinvalidwithformat,rangeorstep. The limitsminandmaxapply to the whole value, the gridstepto the time.A zone label beside the time, linked to both parts as a description. The field neither chooses nor converts a zone.
Usage#
import {LocalDateTimeField} from '@workspace/active-ui/components/composed/local-date-time-field';
<LocalDateTimeField id="send-at" aria-labelledby="send-at-label" value={value} // 'YYYY-MM-DDTHH:mm' or '' onValueChange={setValue} onStateChange={setState} locale={presentation} today={todayIsoDate(timeZone)} openCalendarLabel="Choose a date" dateLabel="Date" timeLabel="Time" timeZoneLabel={timeZone}/>;States and validation#
While the state is incomplete or invalid, the visible text is not the value the form holds.
A form that saves must listen to on and refuse to submit with a message; otherwise a
broken edit saves the previous value unnoticed. A new value from outside, such as a form reset,
ends the edit and reports the state again.
What your application provides#
The conversion to and from an instant, in the owner's zone and under its policy for a skipped or repeated clock time (
wallandClock To Instant instantin active-temporal).To Wall Clock today, the presentation, the translated names of both parts and of the calendar button, and the zone label when the zone matters to the reader.
Accessibility and keyboard#
The pair is a
role="group"named byaria-: pass thelabelledby idof the visible label.idlands on the date input, soFieldfocuses it; each part is named byLabel html For dateandLabel time.Label The date input, the calendar button and the time input are three tab stops.
API#
Import from @workspace/
Props
- Type
string
Accessible name of the date part (Date). Supplied translated.
- Type
DatePresentation
The presentation of both parts: date notation and calendar words, and the clock (time H24/H12, else the language's). The same presentation the rest of the page formats with.
- Type
string
Accessible name of the calendar button. Supplied translated.
- Type
string
Accessible name of the time part (Time). Supplied translated.
- Type
string
The owner's calendar day, as for Date.
- Type
string
The chosen wall-clock value as a Local (YYYY-, with seconds at second precision), or ''.
A wall clock, not a moment. No zone travels with it and this field invents none. The owner turns it into an instant in ITS zone, under ITS policy for a skipped or repeated clock time (wall in @workspace/), and reads a stored instant back with instant in the same zone.
- Type
string
Lands on both inputs.
- Type
boolean
Marks both parts as invalid.
- Type
string
Lands on the group, so the field's visible label names the pair.
- Type
string
Lands on the group.
- Type
boolean- Default
false
Disables both parts and the calendar button.
- Type
string
Lands on the DATE input, so <Field focuses it.
- Type
LocalDate Time String
The latest allowed value, inclusive.
- Type
LocalDate Time String
The earliest allowed value, inclusive.
- Type
string
The base of the inputs' names: <name>. and <name>..
- Type
TimeInput Precision - Default
'minute'
minute (default) or second.
- Type
boolean- Default
false
Makes both parts read-only and the calendar button unavailable.
- Type
boolean- Default
false
Marks both parts as required for assistive technology.
- Type
TimeInput Step
The allowed time grid, counted from midnight of each day ({unit: 'minute', value: 15}). Never rounded to: a time off the grid is invalid (step). A step in seconds needs precision: 'second'.
- Type
string
Text shown after the time, for example the zone the owner reads the value in (Europe/). Supplied by the host when the zone matters to the reader; the field neither chooses nor converts a zone. It is linked to both parts as a description.
Events
Called by the component when something happens.
- Type
(value: string) => void
Receives the new value, or ''. Only a complete, valid date AND time is a value; clearing BOTH parts is ''. A half-filled pair (a date without a time) or invalid text never travels out, so it cannot erase a stored value; on reports it instead.
- Type
(state) => void
Receives the state of the pair on every edit, and again when a new value from outside (a form reset) ends an edit.
Three things that are not the same. The *edit* is the text in the two parts; the *value* is the last complete, valid pair (value), which an edit in progress never overwrites; and whether the form may be *submitted* is the host's question. While the state is incomplete or invalid, the visible text is not the value the form holds, so a form that saves must listen here and refuse to submit with a field message: without that, a cleared or broken edit saves the previous value unnoticed.
Takes only the props listed.
Types
The state of the two parts together, as the field reports it.
type LocalDateTimeFieldState =
| {status: 'empty' | 'incomplete' | 'valid'}
| {status: 'invalid'; rejection: 'format' | 'range' | 'step'};The props of Local, each described in its prop list on this page.
interface LocalDateTimeFieldProps {
value: string;
onValueChange: (value: string) => void;
onStateChange?: (state: LocalDateTimeFieldState) => void;
locale: DatePresentation;
today: string;
openCalendarLabel: string;
dateLabel: string;
timeLabel: string;
precision?: TimeInputPrecision;
step?: TimeInputStep;
min?: LocalDateTimeString;
max?: LocalDateTimeString;
timeZoneLabel?: string;
disabled?: boolean;
readOnly?: boolean;
required?: boolean;
id?: string;
'aria-labelledby'?: string;
'aria-describedby'?: string;
'aria-invalid'?: boolean;
name?: string;
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
className?: string;
}