---
title: Datepicker
description: A single-date picker with an MD3 modal calendar or an iOS inline calendar sheet.
---
import Source from './_generated/datepicker.mdx';

A date form control with three modes — a single date, a date **and time**, or a date **range**. The trigger renders as an input-like field showing the formatted value; opening reveals a platform-appropriate calendar — a centered Material 3 modal (headline, month/year navigation, Cancel/OK) and an iOS-style inline calendar presented in a bottom sheet (Cancel/Done toolbar). One component, forked entirely by the cascade.

<DemoFrame component="datepicker" height={560} />

## Installation

```bash
npx touchcn add datepicker
```

## Usage

<CodeGroup>

```html Angular
<tcn-datepicker [(value)]="date" label="Birthday" placeholder="Choose a date" />
```

```tsx React
import { TcnDatepicker } from '@/components/ui/datepicker';

<TcnDatepicker value={date} onValueChange={setDate} label="Birthday" placeholder="Choose a date" />
```

```vue Vue
<script setup lang="ts">
import { TcnDatepicker } from '@/components/ui/datepicker';
</script>

<template>
  <TcnDatepicker v-model="date" label="Birthday" placeholder="Choose a date" />
</template>
```

</CodeGroup>

### Constraining the selectable days

`min` and `max` take ISO date strings and disable out-of-range days in every mode; the month chevrons stop at the bounds.

<CodeGroup>

```html Angular
<tcn-datepicker [(value)]="date" [min]="'2024-01-01'" [max]="'2024-12-31'" />
```

```tsx React
<TcnDatepicker value={date} onValueChange={setDate} min="2024-01-01" max="2024-12-31" />
```

```vue Vue
<TcnDatepicker v-model="date" min="2024-01-01" max="2024-12-31" />
```

</CodeGroup>

## Modes

The `mode` prop selects what the picker collects. Because the two range endpoints are typed differently from a single value, `range` mode uses its own `range` / `onRangeChange` (React) or `[(range)]` (Angular) model — keeping single-date usage precisely typed and unchanged.

### Date & time (`datetime`)

Adds a time section below the calendar. On Material this is an MD3 time **input** (two `HH` / `MM` fields, with an AM/PM segmented control on 12-hour locales — the locale's hour cycle is detected via `Intl`). On iOS it is the classic momentum wheel — hour / minute drums powered by the shared [Picker](/docs/components/picker) engine, with correct centering, momentum and snap, and the selected value scrolled into view on open. The committed value is an ISO **local datetime** string, `YYYY-MM-DDTHH:mm`.

<CodeGroup>

```html Angular
<tcn-datepicker [(value)]="reminder" mode="datetime" label="Reminder" />
```

```tsx React
<TcnDatepicker value={reminder} onValueChange={setReminder} mode="datetime" label="Reminder" />
```

```vue Vue
<TcnDatepicker v-model="reminder" mode="datetime" label="Reminder" />
```

</CodeGroup>

### Date range (`range`)

Tap a start day, then an end day; the span between highlights, and the value is a `{ start, end }` pair of ISO dates (ordered chronologically regardless of tap order). Tapping again after a complete range starts a new one.

<CodeGroup>

```html Angular
<tcn-datepicker [(range)]="stay" mode="range" label="Stay" />
```

```tsx React
<TcnDatepicker range={stay} onRangeChange={setStay} mode="range" label="Stay" />
```

```vue Vue
<TcnDatepicker v-model:range="stay" mode="range" label="Stay" />
```

</CodeGroup>

## The value is an ISO string, not a `Date`

The picker takes and emits ISO **strings** — never a JavaScript `Date`. A `Date` carries a time and an implicit timezone, which silently shifts a "date" across midnight (a day picked in UTC+13 can read as the previous day in UTC). An ISO string has no zone, so the value you store is exactly what the user tapped:

- `date` — an ISO calendar day, `YYYY-MM-DD`.
- `datetime` — an ISO local datetime, `YYYY-MM-DDTHH:mm` (24-hour, no seconds, **no timezone**). It has deliberate local wall-clock semantics: apply the user's timezone at the edge (storage / display) if you need an absolute instant.
- `range` — a `{ start, end }` pair of ISO calendar days.

Internally the calendar math (in `@touchcn/core`) uses a local `Date` at midnight purely for whole-day arithmetic — no zone conversion ever crosses the API boundary.

Month and weekday names, and the first day of the week, come from `Intl.DateTimeFormat` / `Intl.Locale` (falling back to Monday); pass a `locale` prop to override the browser default. There is no date-library dependency.

## Accessibility

The calendar is a WAI-ARIA `grid`. Arrow keys move focus by day and week (crossing month boundaries), `PageUp` / `PageDown` change month, `Home` / `End` jump to the week bounds, and `Enter` / `Space` select the focused day. In `range` mode every cell within the selection carries `aria-selected` (both endpoints and the band between), per the WAI date-range grid guidance. The time inputs are labelled (`Hour` / `Minute`, `AM or PM`). Focus is trapped in the overlay while open and restored to the trigger on close.

## Props

| Prop            | Type                                | Default         | Description                                             |
| --------------- | ----------------------------------- | --------------- | ------------------------------------------------------ |
| `mode`          | `'date' \| 'datetime' \| 'range'`   | `'date'`        | What the picker collects.                               |
| `value`         | `string \| null`                    | `null`          | `date` / `datetime` value (two-way on Angular).         |
| `onValueChange` | `(value: string) => void`           | —               | `date` / `datetime` selection callback (React).         |
| `range`         | `{ start, end } \| null`            | `null`          | `range` value (two-way `[(range)]` on Angular).         |
| `onRangeChange` | `(range: { start, end }) => void`   | —               | `range` selection callback (React).                     |
| `min`           | `string \| null`                    | `null`          | Earliest selectable day (ISO); earlier days disabled.   |
| `max`           | `string \| null`                    | `null`          | Latest selectable day (ISO); later days disabled.       |
| `locale`        | `string`                            | browser locale  | BCP-47 locale for names, week start, and 12/24-hour time. |
| `label`         | `string`                            | `''`            | Field label.                                            |
| `placeholder`   | `string`                            | `'Select date'` | Shown when nothing is selected.                         |
| `disabled`      | `boolean`                           | `false`         | Disable the control.                                    |

## Future work

A couple of items are deliberately deferred to a future round:

- **MD3 clock-dial** time picker — out of scope; the MD side ships the spec's time-**input** variant. (The iOS momentum wheel is now shipped via the [Picker](/docs/components/picker) engine.)
- Month/year navigation stays at month granularity.

<Source />
