DateField
A text field for a calendar day in the reader's notation, with a calendar beside it, that hands your application an ISO date.
Use Date for a calendar day: a date of birth, a start date, a deadline. The reader types the
day in the notation of the presentation you pass, or picks it from a calendar. Your application
receives the day as YYYY-, never as a Date, so no time zone can shift it.
Value: 2026-03-09 · state: valid
// apps/docs/examples/ui/date-field.tsx'use client';
import {useState} from 'react';
import { composePresentationContext, toIanaTimeZone, type DateInputStatus,} from '@workspace/active-format';import {DateField} from '@workspace/active-ui/components/composed/date-field';import { Field, FieldDescription, FieldError, FieldLabel,} from '@workspace/active-ui/components/primitives/field';import { NativeSelect, NativeSelectOption,} from '@workspace/active-ui/components/primitives/native-select';
// A fixed "today", so every reader sees the same month.const TODAY = '2026-10-20';const ZONE = toIanaTimeZone('Europe/Amsterdam');
const PRESENTATIONS = { en: composePresentationContext('en', {}, ZONE), nl: composePresentationContext('nl', {}, ZONE), 'en, MDY': composePresentationContext( 'en', {dateFormat: 'MDY_NUMERIC'}, ZONE, ),} as const;
/** * A calendar day, typed in the reader's notation or picked from the * calendar. The value is always `YYYY-MM-DD`; the text follows the * presentation. Half-typed or impossible text keeps the last value and is * reported as a state, so it can never erase a stored date. */export default function DateFieldExample() { const [presentation, setPresentation] = useState<keyof typeof PRESENTATIONS>('en'); const [value, setValue] = useState('2026-03-09'); const [status, setStatus] = useState<DateInputStatus>('valid'); return ( <div className="grid w-full max-w-xs gap-6"> <Field> <FieldLabel htmlFor="date-presentation">Presentation</FieldLabel> <NativeSelect id="date-presentation" className="w-full" value={presentation} onChange={(event) => setPresentation(event.target.value as keyof typeof PRESENTATIONS) } > {Object.keys(PRESENTATIONS).map((key) => ( <NativeSelectOption key={key} value={key}> {key} </NativeSelectOption> ))} </NativeSelect> </Field> <Field data-invalid={status === 'invalid' || undefined}> <FieldLabel htmlFor="date-start">Start date</FieldLabel> <DateField id="date-start" value={value} onValueChange={setValue} onStateChange={setStatus} locale={PRESENTATIONS[presentation]} today={TODAY} openCalendarLabel="Choose a date" aria-invalid={status === 'invalid' || undefined} aria-describedby="date-start-state" /> {status === 'invalid' ? ( <FieldError id="date-start-state"> That date does not exist. </FieldError> ) : ( <FieldDescription id="date-start-state"> Value: <code>{value || "''"}</code> · state: {status} </FieldDescription> )} </Field> </div> );}What the composition adds#
Date is an Input with a Popover
holding a Calendar, plus the date reading of
active-format. Compared with <input type="date">, it:
shows and reads the notation of the presentation you pass, not the browser's language: a native date field follows the browser, whatever the page's language;
takes the order of day, month and year from the notation and the words of the calendar from the language, so
MDY_with Dutch showsNUMERIC 12/beside Dutch month names;31/ 2026 parses strictly:
31-is rejected, never moved to another day, and the field never tries a second order;02- 2026 9-is accepted and shown as3- 2026 09-when the field loses focus;03- 2026 keeps empty, incomplete, invalid and valid apart: only empty (
'') and valid are a value, so half-typed text never erases a stored date.
Usage#
import {DateField} from '@workspace/active-ui/components/composed/date-field';
<DateField id="start" value={value} // 'YYYY-MM-DD' or '' onValueChange={setValue} onStateChange={setStatus} locale={presentation} today={todayIsoDate(timeZone)} openCalendarLabel="Choose a date"/>;States and validation#
on reports the typed text on every keystroke as empty, incomplete, invalid or
valid. Use it to block saving while the text cannot be read; show the message only for
invalid, so the field does not complain while someone is still typing. Unreadable text stays
visible after the field loses focus and the input gets aria-.
Rules about the date itself, such as an end date after a start date, belong in your form's schema.
The field has no min, max or excluded days.
What your application provides#
today, the current day of whoever owns the data, asYYYY-. The field reads no clock: it marks this day in the calendar, opens an empty field on its month and centres the year range on it.MM- DD The presentation (
locale), the translated name of the calendar button and the form binding.A visible label with
FieldtheLabel html For id, and the messages.
class lands on the wrapper, so a width class keeps the calendar button attached; every other
prop goes to the text input.
Accessibility and keyboard#
The text input is an ordinary text field with
input.Mode="numeric" The calendar button is a separate tab stop named by
open; in the calendar the arrow keys move by day and week, and choosing a day closes it and fills the field.Calendar Label
API#
Import from @workspace/
Props
- Type
DatePresentation
The presentation of this field: a bare language code ('nl') or a generic presentation-context object that carries language and notation independently ({language).
Two things that are not the same. The *structural* shape of the input -- the order of day, month and year and the separator, and with that also the placeholder and the parsing -- comes from the notation. The *words* of the calendar -- month names, weekday names -- come from the language. A bare language code makes the two coincide; a presentation context lets them diverge as they should: an owner on MDY_ shows 12/ in the field while the calendar keeps Dutch month names. Taking the order from the language would show the same date in two shapes next to a grid that follows the notation.
Deliberately a prop and not use inside: that mirrors the read side, where every formatter receives its presentation as an argument, and it keeps this component generic -- active- knows no administration, tenant or profile resolver. The app layer composes (use), this component receives.
- Type
string
Accessible name of the calendar button. Supplied translated; active- does not translate.
- Type
string
The current calendar day of the surface's owner, as ISO YYYY-.
Required, and not the runtime's clock. It drives the calendar's today marker, the month an empty field opens on and the navigation range, which is centred on this day's year. Which day is today follows from the time zone of whoever owns the surface; a browser in another zone, or a render just past midnight or New Year, would otherwise mark a different day and bound a different range. The caller derives it with today; this component reads no clock, the same contract as Today..
- Type
string
The chosen calendar day as an ISO date-only value (YYYY-), or '' when there is no date.
Never a `Date`. A Date is an instant on the timeline and carries time-zone behaviour; a date of birth or a start date does not. '' is the empty state — the same shape the native date input itself uses and that every form in this repo already carries.
- Type
string
Lands on the field's wrapper, not on the text input itself.
The calendar button is absolutely positioned within that wrapper, so a width class (max-) on the text input alone would leave the button detached on the right. All other HTML props do go to the text input, id included — so <Field works as for any other field.
- Type
string
Text in the empty field. Defaults to the locale pattern in lower case (dd- for nl, dd. for de).
That is a notation, not prose: nothing is translated and nothing is hard-coded, and by construction the placeholder cannot fall out of step with what the field accepts.
Events
Called by the component when something happens.
- Type
(value: string) => void
Receives the new ISO date-only value, or ''.
`''` means only "cleared". This component does not report unreadable or half-typed text as a value: on an optional field with an existing value, an empty string would be passed on as a deliberate clearing and erase the stored date. The last valid value stays, the text stays visible, and the state travels via Date.
- Type
(status) => void
Receives the state of the typed text on every keystroke.
Intended for the form that wants to prevent saving while unreadable text is in the field. The distinction between incomplete and invalid exists so a field does not report an error while the user is still typing.
Optional: without this prop the component is already safe on its own -- it never clears an existing value on the basis of unreadable text.
Other props, such as the standard attributes of the underlying element, className and ref, are passed through.
Types
The props of Date, each described in its prop list on this page.
interface DateFieldProps extends Omit<
React.ComponentProps<'input'>,
'value' | 'defaultValue' | 'onChange' | 'type'
> {
value: string;
onValueChange: (value: string) => void;
onStateChange?: (status: DateInputStatus) => void;
locale: DatePresentation;
today: string;
openCalendarLabel: string;
placeholder?: string;
className?: string;
}Related#
Calendar, for a calendar on the page