# DatePicker

```tsx
import { DatePicker } from '@tangible/ui';
```

Interactive docs and examples: [DatePicker in Storybook](https://storybook-tangible-ui.pages.dev/?path=/docs/forms-datepicker--docs).

## DatePicker props

| Prop | Type | Default | Description |
| - | - | - | - |
| `aria-describedby?` | `string` | | ID of element(s) that describe the trigger. `Field.Control` injects this so helper text and errors are announced; without it the injection is silently dropped (no rest-spread on this component). |
| `aria-invalid?` | `"false" \| "true" \| boolean` | | Marks the control invalid. Lands on the trigger button, which also wears `tui-input-group` — that is what lights the danger border. |
| `aria-label?` | `string` | | |
| `aria-labelledby?` | `string` | | ID of the element that labels the trigger. `Field.Control` injects this, and it outranks `aria-label` per AccName 2A — which is why the trigger's own `aria-label` fallback does not have to change. The trigger's value span is appended so the current date stays audible — except under `asChild`, where there is no value span and a phantom id would strip the name. |
| `calendarLabels?` | `CalendarLabels` | | |
| `children?` | `ReactNode` | | Trigger placement. Omit and the picker renders its own input-group button, exactly as before. Provide children to place the trigger yourself — inside a text field's suffix slot, a toolbar — with `<DatePicker.Trigger>` marking the spot, and `asChild` on that Trigger to supply your own element: `<DatePicker value={date} onChange={commit}> <TextInput value={text} onChange={(e) => setText(e.target.value)} suffix={ <DatePicker.Trigger asChild> <IconButton icon="lms/calendar" label="Pick a date" /> </DatePicker.Trigger> } /> </DatePicker>` Children with no Trigger among them leaves the picker unopenable; a dev warning fires for that case. |
| `className?` | `string` | | |
| `defaultValue?` | `Date \| null` | | |
| `disabled?` | `boolean` | | |
| `hour12?` | `boolean` | `true` | 12-hour time UI with an AM/PM toggle. |
| `id?` | `string` | | |
| `isDayDisabled?` | `(day: Date) => boolean` | | Per-day disable hook, applied on top of min/max. |
| `labels?` | `DatePickerLabels` | | |
| `locale?` | `string` | `the document language, falling back to 'en-US'` | BCP 47 locale for month/weekday names and numeric formats. |
| `max?` | `Date` | | Latest selectable day (inclusive, time-of-day ignored). |
| `min?` | `Date` | | Earliest selectable day (inclusive, time-of-day ignored). |
| `onChange?` | `(value: Date) => void` | | |
| `value?` | `Date \| null` | | |
| `weekStartsOn?` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6` | `1` | First day of the week. 0 = Sunday … 6 = Saturday. |
| `withTime?` | `boolean` | `false` | Include the time field (and format the trigger with time). |
| `zoneLabel?` | `ReactNode` | | Display-only zone badge next to the time field, e.g. "UTC+0". |

### DatePicker.Trigger

Also exported as `DatePickerTrigger`.

| Prop | Type | Default | Description |
| - | - | - | - |
| `asChild?` | `boolean` | | Render `children` as the trigger instead of the built-in input-group button. The child owns its accessible name and receives no chrome — see this file's header for what that costs and why. |
| `children?` | `React.ReactNode` | | Trigger element, used when `asChild` is set. |
| `className?` | `string` | | |

## Other exports

- `defaultDatePickerLabels`: `Required<DatePickerLabels>`

## Examples

- [With Field Error](https://storybook-tangible-ui.pages.dev/?path=/story/forms-datepicker--with-field-error): DatePicker inside Field with Field.Error. `Field.Control` injects
  `aria-invalid` and `aria-describedby` onto the trigger, which is also the
  element wearing `tui-input-group` — so the shared primitive lights the
  danger border and AT announces the error text.

  `aria-required` is deliberately NOT forwarded: the trigger is
  `aria-haspopup="dialog"` on a plain button, and `role="button"` does not
  support that attribute. Field.Label carries the requirement instead.

- [asChild trigger](https://storybook-tangible-ui.pages.dev/?path=/story/forms-datepicker--as-child-trigger): `DatePicker.Trigger asChild` swaps the built-in input-group button for
  whatever you hand it — the escape hatch for cases where the affordance is an
  icon button beside free text, not a date field. The popover (time field, Now,
  month/day/year, calendar) is unchanged.

  `<DatePicker />` with no children keeps rendering its own trigger, so this is
  additive: reach for `DatePicker.Trigger` only when you need to place the
  trigger yourself.

  Three things differ from the default trigger, all deliberate:
  - The child owns its accessible name, so name it for the *action* ("Pick a
    date") rather than the value — unlike the default trigger, the selected
    date is not appended to the name.
  - No `tui-input-group` chrome is applied (that is the point), so `aria-invalid`
    forwards without painting anything. A custom trigger inside a `Field` with an
    error needs its own `[aria-invalid="true"]` rule — see the AsChildInvalid
    story below.
  - `disabled` arrives as `aria-disabled` + `data-disabled`, since the child may
    not be a `<button>` — see AsChildDisabled below.
  The child must be natively focusable and keyboard-activatable (a `<button>` or
  `<a>`, not a styled `<div>`): under `asChild` the picker attaches no key
  handler of its own. Nothing else changes — `onChange` still hands back a
  `Date`, and the child's own `onClick` still runs.

  The play function opens the popover from the custom trigger: the visual suite
  cannot see portaled content, so this is what proves the cloned ref and props
  wiring survives in a real browser.

- [asChild trigger (disabled)](https://storybook-tangible-ui.pages.dev/?path=/story/forms-datepicker--as-child-disabled): A disabled picker cannot hand the child a native `disabled` — the child may not
  be a `<button>`. It gets `aria-disabled` + `data-disabled` instead, and the
  picker refuses to open, which is what the play function asserts by keyboard —
  the element stays focusable and Enter reaches the handler, where the guard
  turns it away. (A mouse click proves less: IconButton's own `aria-disabled`
  rule sets `pointer-events: none`, so the click never arrives at all.)

  Only the *open* direction is guarded. Blocking close as well would strand an
  open popover when a form disables its controls mid-submit.

- [asChild trigger (invalid, in a Field)](https://storybook-tangible-ui.pages.dev/?path=/story/forms-datepicker--as-child-invalid): `Field.Control` injects `aria-invalid` and `aria-describedby` onto the custom
  trigger exactly as it does the built-in one, and `Field.Label` reaches the
  accessible name through `aria-labelledby`. What does *not* come along is the
  danger border: it belongs to `tui-input-group`, which `asChild` deliberately
  does not apply.

  So an invalid custom trigger needs one rule of your own — the attribute is
  already on the element, it just isn't painted:
  ```css
  .my-date-trigger[aria-invalid='true'] {
    border-color: var(--tui-theme-danger-base);
    color: var(--tui-theme-danger-base);
  }
  ```

- [Composed into a text field](https://storybook-tangible-ui.pages.dev/?path=/story/forms-datepicker--composed-into-text-field): The composition the compound shape exists for: a text field the user can type
  into freely, with the picker hanging off its suffix slot. Typed text like
  "3pm ET" survives — no picker produces a timezone label — and the calendar
  button fills in what a picker *is* good at.

  TUI never writes into the field. `onChange` hands back a `Date` and the
  consumer decides what string to insert, which is what makes this work with a
  format TUI knows nothing about.

  The input group's own click handler focuses the inner input, but bails on
  anything inside a `button` (`TextInput.tsx`'s `handleGroupClick`), so the
  trigger keeps its click and the caret stays put.

Other stories: Default, With time (Pick a Date and Time), Disabled, Min/Max bounds ([interactive page](https://storybook-tangible-ui.pages.dev/?path=/docs/forms-datepicker--docs)).

## Component API tokens

Read via fallback, never declared. Set one on an ancestor to restyle every instance under it.

- `--tui-calendar-cell-size`
- `--tui-calendar-cell-radius`
- `--tui-calendar-selected-bg`
- `--tui-calendar-selected-fg`
- `--tui-calendar-range-bg`
- `--tui-calendar-today-bg`
- `--tui-calendar-caption-color`
- `--tui-date-picker-width`

---

Interactive version: https://storybook-tangible-ui.pages.dev/?path=/docs/forms-datepicker--docs
