---
id: "packages/active-ui/date-field"
title: "DateField"
description: "A text field for a calendar day in the reader's notation, with a calendar beside it, that hands your application an ISO date."
url: "/packages/active-ui/date-field"
locale: "en"
revision: "bfcd2d95bae2faf3"
---

# DateField

Use `DateField` 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-MM-DD`, never as a `Date`, so no time zone can shift it.

**Example: A calendar day in three presentations**

Client: Runs entirely in your browser. Source: `examples/ui/date-field.tsx`.

```tsx
// 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

`DateField` is an [`Input`](/packages/active-ui/input) with a [`Popover`](/packages/active-ui/popover)
holding a [`Calendar`](/packages/active-ui/calendar), plus the date reading of
[active-format](/packages/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_NUMERIC` with Dutch shows `12/31/2026` beside Dutch month names;
- parses **strictly**: `31-02-2026` is rejected, never moved to another day, and the field never
  tries a second order; `9-3-2026` is accepted and shown as `09-03-2026` when the field loses
  focus;
- keeps **empty, incomplete, invalid and valid** apart: only empty (`''`) and valid are a value,
  so half-typed text never erases a stored date.

## Usage

```tsx
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

`onStateChange` 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-invalid`.

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, as `YYYY-MM-DD`. 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.
- The presentation (`locale`), the translated name of the calendar button and the form binding.
- A visible label with `FieldLabel htmlFor` the `id`, and the messages.

`className` 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 `inputMode="numeric"`.
- The calendar button is a separate tab stop named by `openCalendarLabel`; in the calendar the
  arrow keys move by day and week, and choosing a day closes it and fills the field.

## API

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `locale` | `DatePresentation` | yes | The presentation of this field: a bare language code (`'nl'`) or a generic presentation-context object that carries language and notation independently (`{languageLocale: 'nl', dateFormat: 'MDY_NUMERIC'}`).  **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_NUMERIC` shows `12/31/2026` 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 `usePresentation()` inside: that mirrors the read side, where every formatter receives its presentation as an argument, and it keeps this component generic -- `active-ui` knows no administration, tenant or profile resolver. The app layer composes (`usePresentation()`), this component receives. |
| `onValueChange` | `(value: string) => void` | yes | 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 {@link DateFieldProps.onStateChange}. |
| `openCalendarLabel` | `string` | yes | Accessible name of the calendar button. Supplied translated; `active-ui` does not translate. |
| `today` | `string` | yes | The current calendar day of the surface's owner, as ISO `YYYY-MM-DD`.  **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 `todayIsoDate(useRequiredTimeZone())`; this component reads no clock, the same contract as `Today.today`. |
| `value` | `string` | yes | The chosen calendar day as an ISO date-only value (`YYYY-MM-DD`), 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. |
| `className` | `string` | no | 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-w-xs`) 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 `<FieldLabel htmlFor={id}>` works as for any other field. |
| `onStateChange` | `((status: DateInputStatus) => void)` | no | 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. |
| `placeholder` | `string` | no | Text in the empty field. Defaults to the locale pattern in lower case (`dd-mm-yyyy` for `nl`, `dd.mm.yyyy` 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. |

**DateFieldProps**

```ts
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

- [TimeField](/packages/active-ui/time-field) and
  [LocalDateTimeField](/packages/active-ui/local-date-time-field)
- [Calendar](/packages/active-ui/calendar), for a calendar on the page
