<!-- Protocore Design System — DateInput -->
# DateInput

- **Category:** Inputs (`inputs`)
- **Slug:** `inputs/date-input`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { DateInput } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/inputs/date-input

> Segmented day/month/year field with spinbutton segments — the keyboard-first, type-to-fill alternative to the calendar.

## When to use it

Use **DateInput** whenever users know the date and would rather type it than hunt through a grid — dates of birth, invoice dates, anything far from today. It is the most accessible date control in the library: no popover, no pointer required, every segment reachable by keyboard. Pair it with a **DatePicker** when some users prefer to browse a calendar; they share the same `value` contract. Use **Calendar** when the grid should be visible inline.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `aria-label` | `string` | no | — | Accessible group label describing the field (e.g. "Invoice date"). |
| `aria-labelledby` | `string` | no | — | Id of an element labelling the field (e.g. a `Field` label). |
| `className` | `string` | no | — | — |
| `defaultValue` | `Date \| null` | no | — | Initial value in uncontrolled mode. |
| `disabled` | `boolean` | no | — | Disable the whole field. |
| `invalid` | `boolean` | no | — | Mark the field invalid — danger border plus `aria-invalid`. |
| `locale` | `string` | no | — | BCP-47 locale controlling segment order and separators. |
| `max` | `Date` | no | — | Latest valid day (inclusive). |
| `min` | `Date` | no | — | Earliest valid day (inclusive) — a complete date outside the range is treated as invalid. |
| `onValueChange` | `((date: Date \| null) => void)` | no | — | Fires with a complete valid `Date`, or `null` when the field is cleared/incomplete. |
| `size` | `enum` | no | `md` | Field height: `sm` (32) · `md` (36, default) · `lg` (40). |
| `style` | `CSSProperties` | no | — | — |
| `value` | `Date \| null` | no | — | Controlled value. Pass `null` for an empty field. |

## Examples

### Basics

Each of the day, month, and year segments is a spinbutton. Type digits to fill (focus auto-advances when a segment is full) or use the arrow keys to step. A complete, valid date is emitted via `onValueChange`; incomplete input emits `null`.

```tsx
import { useState } from "react";
import { DateInput } from "@protocore/pds";

export default function Basics() {
  // Type digits (focus auto-advances) or arrow-step each segment. onValueChange
  // fires only when the three segments form a valid date.
  const [value, setValue] = useState<Date | null>(new Date(2026, 6, 6));

  return (
    <div style={{ display: "grid", gap: 12, justifyItems: "start" }}>
      <DateInput aria-label="Invoice date" value={value} onValueChange={setValue} locale="en-GB" />
      <span style={{ font: "13px system-ui", opacity: 0.7 }}>
        {value ? value.toDateString() : "Incomplete"}
      </span>
    </div>
  );
}
```

### Locale-aware order

Segment order and separators come from the `locale` via Intl.DateTimeFormat — `en-US` renders month · day · year with slashes, `de-DE` renders day · month · year with dots. Nothing to configure beyond the locale.

```tsx
import { DateInput } from "@protocore/pds";

export default function Locales() {
  // Segment order + separators follow the locale — no manual masking.
  const value = new Date(2026, 6, 6);

  return (
    <div style={{ display: "grid", gap: 16, justifyItems: "start" }}>
      <label style={{ display: "grid", gap: 4 }}>
        <span style={{ font: "11px ui-monospace", letterSpacing: "0.06em", opacity: 0.6 }}>
          en-US — MM / DD / YYYY
        </span>
        <DateInput aria-label="US date" defaultValue={value} locale="en-US" />
      </label>
      <label style={{ display: "grid", gap: 4 }}>
        <span style={{ font: "11px ui-monospace", letterSpacing: "0.06em", opacity: 0.6 }}>
          de-DE — DD . MM . YYYY
        </span>
        <DateInput aria-label="German date" defaultValue={value} locale="de-DE" />
      </label>
    </div>
  );
}
```

## Do & don't

**Do**

- Give the field an accessible group name via a `Field` label or `aria-label`.
- Prefer DateInput for distant dates (birthdays, historical records) — typing beats scrolling.
- Let the locale drive segment order instead of hard-coding DD/MM/YYYY.
- Read the emitted `Date` (or `null`) rather than parsing the rendered text.

**Don't**

- Don't add your own separators or masking on top — the segments own formatting.
- Don't treat an incomplete field as a date; `onValueChange` reports `null` until it's valid.
- Don't remove the arrow-key stepping — it's a core affordance of a spinbutton.
- Don't wrap each segment in its own label; the whole field is one labelled group.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `0–9` | Fill the focused segment; focus auto-advances when it's full. |
| `Arrow Up / Down` | Increment / decrement the focused segment (wraps at the bounds). |
| `Arrow Left / Right` | Move between segments. |
| `Backspace / Delete` | Clear the focused segment. |

**Notes**

- Each segment is a `role="spinbutton"` with `aria-valuemin` / `aria-valuemax` / `aria-valuenow` and a name (Day, Month, Year), so screen readers announce it like a native stepper.
- The segments are grouped in a `role="group"` that carries the field's accessible name and `aria-invalid`.
- No pointer is required for any operation — the control is fully keyboard-operable.

## Related

`date-picker`, `calendar`, `field`, `number-input`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
