# Date field

A date typed as text, one field for the whole date: "12/11/2026". It reads the text in the order the person's locale writes dates (day first in the UK, month first in the US), and always reads ISO `2026-11-12` too, so a date pasted from anywhere reads the same. The placeholder is the locale's pattern ("dd/mm/yyyy").

It reads the text when the person leaves the field or presses Enter, never while they type. Then it rewrites the text in the locale's format and says under the field, in words, which date it read ("Thursday, 12 November 2026"), so a day-first or month-first reading is never silent. Text it can't read is never guessed into a date: it says so, with the pattern to write, and `onValueChange` isn't called. A day past the month's end ("31/02") and a date outside `min` and `max` each get their own message. The messages and the reading describe the input, so they are read when focus returns to it; they aren't announced as the person moves on.

`onValueChange` gets the day at local midnight, or `null` when the field is emptied. A date outside `min` or `max` is still reported, marked invalid, so the app's value is the one the field shows; check it before acting on it. With `name`, a hidden input carries ISO `yyyy-mm-dd` for a form (empty while the text can't be read). Use `Field` for a hint and the app's errors, or `label` for a plain label. For a calendar to pick from, use `DatePicker`.

- Registry name: `date-field` (`@candor/date-field`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/date-field

## Install

```bash
npx candor-ui add date-field
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/date-field`.
It brings in: bidi, input, label, locale, locked, tokens, utils.

## Example

```tsx
"use client"

import * as React from "react"
import { DateField, DateRangeField, type DateFieldRange } from "@/components/candor/ui/date-field"
import { Field } from "@/components/candor/ui/field"

// Typed dates are read in the order the person's locale writes them, and ISO always; the field says in words which
// date it read, so 03/04 is never silently the wrong one. Text it can't read is never guessed into a date.
export default function Example() {
  const [due, setDue] = React.useState<Date | null>(null)
  const [period, setPeriod] = React.useState<DateFieldRange>({ from: null, to: null })
  return (
    <div className="flex w-full max-w-md flex-col gap-6">
      <Field label="Finish by" hint="The agent plans its work to end on this day.">
        <DateField value={due} onValueChange={setDue} min={new Date(2026, 0, 1)} name="due" />
      </Field>
      <DateRangeField label="Report period" value={period} onValueChange={setPeriod} />
    </div>
  )
}
```

## <DateField>

Props:

- `id?: string | undefined`: The input's id, for a label's `htmlFor`. `Field` passes one.
- `"aria-describedby"?: string | undefined`: Ids of anything else that describes the field (a hint, an error); the field's own reading or message is added after them.
- `"aria-invalid"?: boolean | "true" | "false" | undefined`: Marks the field invalid for an error of the app's (from `Field`), as well as for the field's own messages.
- `"aria-required"?: boolean | "true" | "false" | undefined`: The field must be filled (from `Field`'s `required`).
- `"aria-label"?: string | undefined`: Names the input when there is no visible label.
- `attend?: boolean | undefined`: Plays one attention ring on the input, as `Input`'s `attend` does, after a failed submit (`Field` passes it).
- `onAnimationEnd?: React.AnimationEventHandler | undefined`: Called when the input's ring ends (`Field` passes it to clear the ring).
- `value?: Date | null | undefined`: The date, when you control it; `null` is empty.
- `defaultValue?: Date | null | undefined`: The date at first, when you don't control it.
- `onValueChange?: ((date: Date | null) => void) | undefined`: Called with the date the field read (at local midnight), or `null` when the person empties it. Never called for text it couldn't read.
- `min?: Date | undefined`: The earliest date allowed. An earlier one is reported, marked invalid and says so.
- `max?: Date | undefined`: The latest date allowed. A later one is reported, marked invalid and says so.
- `label?: string | undefined`: A visible label above the field. Leave it out inside `Field`, which draws its own.
- `locked?: string | null | undefined`: Why the date can't be changed right now. The field stays focusable and readable, can't be typed in, and says why.
- `required?: boolean | undefined`: The field must be filled: sets `aria-required` (the browser's own bubble isn't used).
- `name?: string | undefined`: The form field's name: a hidden input carries the date as ISO `yyyy-mm-dd`.
- `error?: string | null | undefined`: A problem the app found with the date, shown under the field after the reading, and marking it invalid.
- `className?: string | undefined`: Classes for the field's outer box.

## <DateRangeField>

Two `DateField`s for a range, the start and the end, in a `fieldset` whose legend is `label`. Each reads and says its date as `DateField` does. When both are read and the end is before the start, the end field says "The end is before the start." and is marked invalid, and `onValueChange` still reports both: the app decides what to do, and the field has said so. `min` and `max` apply to both ends.

Props:

- `value?: DateFieldRange | undefined`: The range, when you control it.
- `defaultValue?: DateFieldRange | undefined`: The range at first, when you don't control it. Left out, both ends are empty.
- `onValueChange?: ((range: DateFieldRange) => void) | undefined`: Called with both ends whenever the person changes either.
- `min?: Date | undefined`: The earliest date either end can be.
- `max?: Date | undefined`: The latest date either end can be.
- `label: string`: The range's name, the fieldset's legend: "Report period".
- `labels?: { from?: string | undefined; to?: string | undefined } | undefined`: The two fields' labels. Default "From" and "To".
- `locked?: string | null | undefined`: Why the range can't be changed right now; both fields say why.
- `required?: boolean | undefined`: Both ends must be filled.
- `className?: string | undefined`: Classes for the fieldset.

## Strings (`dateField`)

The fixed words, English by default. Override any of them under `dateField` in `<LocaleProvider strings={{ dateField: { … } }}>`; the table is exported as `dateFieldStrings`.

- `day`: dd
- `month`: mm
- `year`: yyyy
- `unreadable`: Couldn't read “{text}” as a date. Write it as {pattern}.
- `pastMonthEnd`: {month} has {days} days.
- `beforeMin`: Choose a date on or after {min}.
- `afterMax`: Choose a date on or before {max}.
- `from`: From
- `to`: To
- `endBeforeStart`: The end is before the start.

## Types

### DateFieldRange

A range of two days; either end is `null` while it is empty.

```ts
{ from: Date | null; to: Date | null }
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
