# DatePicker

**When to use:** Date selection input — booking forms, birth date fields, date range pickers, deadline selectors. Renders as a text input with a floating label by default; can also be inline or modal.

**Keywords:** calendar picker, timezone, ISO date, date string, day picker

**Keyboard:** the day grid uses a roving tab stop (the selected day, or today, or the 1st — one Tab reaches it, not every day individually); arrow keys move by day (`←`/`→`) or by week (`↑`/`↓`), crossing into the adjacent month automatically when they run past the edge, and Enter/Space selects the focused day.

**Import:** `import { DatePicker, dateToDay, dayToDate, dateToTime, dayTimeToDate } from '@ahrowe/ui'`  
**Types:** `import type { DatePickerProps, DatePickerLabels } from '@ahrowe/ui'`

```tsx
import { DatePicker, FormValidator, dateToDay } from '@ahrowe/ui';

// Controlled input with floating label. The value is a day: '2026-09-15'
const [day, setDay] = useState<string | null>(null);
<DatePicker label="Date of birth" selected={day} onSelect={setDay} />

// With FormValidator
const dayValidator = new FormValidator<string | null>(null);
<DatePicker label="Start date" formValidator={dayValidator} />

// Inline calendar (no input field)
<DatePicker selected={day} onSelect={setDay} isInline noInput />

// Inside a modal (calendar opens in a Modal overlay)
<DatePicker label="Schedule date" selected={day} onSelect={setDay} isModal />

// From today to the end of the year, with a clear button
<DatePicker
  label="Appointment"
  selected={day}
  onSelect={setDay}
  minDate={dateToDay(new Date())}
  maxDate="2026-12-31"
  showClearButton
/>

// Range: two pickers sharing a highlighted start and end
<DatePicker label="From" selected={from} onSelect={setFrom} startDate={from} endDate={to} />
<DatePicker label="To" selected={to} onSelect={setTo} startDate={from} endDate={to} minDate={from} />
```

**A day, not a moment:** every day DatePicker takes or hands out is a `'YYYY-MM-DD'` string, the value an `<input type="date">` has. It has no time and no time zone, so it is the same day for everyone who sees it. The strings sort as the days do, so `from < to` compares two of them. `null`, `''` and anything that is not a real day, such as `'2026-02-30'`, mean no value; anything but the first two also logs a warning naming the prop, once.

`dateToDay(date)` and `dayToDate(day)` convert to and from a `Date` in the browser's time zone: `dateToDay(new Date())` is today, `dayToDate(day)` the first moment of that day. Do not use `date.toISOString().slice(0, 10)`: that is the day in UTC, which in Vienna shortly after midnight is still yesterday.

**Key props:**

| Prop | Type | Description |
|------|------|-------------|
| `label` | `string` | Floating label on the input |
| `selected` | `string \| null` | Selected day, `'YYYY-MM-DD'` |
| `onSelect` | `(day: string \| null) => void` | Called when a day is picked in the calendar |
| `onChange` | `(day: string \| null) => void` | Called on every change, picked or typed |
| `formValidator` | `FormValidator<string \| null>` | Connects to validation, and then owns the value. `new FormValidator(null)` and `new FormValidator('')` fit too |
| `isRequired` | `boolean` | |
| `isValid` | `boolean` | Manual valid state |
| `errorMessage` | `string` | Manual error message. A non-empty message puts the field in error by itself, so pass it only while there is one |
| `noInput` | `boolean` | Hide the text input (show calendar only) |
| `noHeader` | `boolean` | Hide the month/year navigation header |
| `isModal` | `boolean` | Open calendar in a Modal overlay |
| `isInline` | `boolean` | Render calendar always-visible inline |
| `minDate` | `string \| null` | Earliest selectable day |
| `maxDate` | `string \| null` | Latest selectable day |
| `showClearButton` | `boolean` | Show a clear/reset button |
| `startDate` | `string \| null` | Range highlight start |
| `endDate` | `string \| null` | Range highlight end |
| `locale` | `string` | Month and weekday names and the first day of the week (default the app's `locale`, see [ConfigProvider.md](ConfigProvider.md), else the browser's). The field is typed as `dd.mm.yyyy` in every locale |
| `weekStartsOn` | `0` to `6` | First day of the week, `0` being Sunday (default the locale's, else Monday) |
| `labels` | `DatePickerLabels` | The text DatePicker renders of its own (see **Labels** below) |

**Slots:** `root` `input` `header` `monthDropdown` `yearDropdown` `calendar` `clearButton`

**Labels:** `previousMonth` `nextMonth` `month` `year` `clear` — the text this component renders of its own. Month and weekday names come from `locale`, not from here. Pass `labels` to override any of them, on the component or app-wide through `ConfigProvider`; they merge per key. Type: `DatePickerLabels`.

The root also takes the full native attribute set (`id`, `data-*`, `aria-*`, `title`, event handlers). `onChange` and `onSelect` report a day, so the native handlers of those names are not available.

## When a day stands for a moment

A poll that closes on a day, a booking that starts on one, a deadline: here the day has to become a moment, and only your app knows in which time zone and at which point of the day. Store the moment itself (a UTC timestamp), and show it in the time zone of whoever reads it.

In the user's own time zone the platform is enough:

```ts
const start = dayToDate(day); // the first moment of the day, where the user is
```

In a given zone, a company's, a household's, a venue's, use a library that knows time zones, such as Luxon. Name the zone (`'Europe/Vienna'`), not its offset (`+02:00`): the offset changes with daylight saving.

```ts
import { DateTime } from 'luxon';

// Closes at the end of the day in Vienna, wherever the reader is.
const closesAt = DateTime.fromISO(day, { zone: 'Europe/Vienna' }).endOf('day').toJSDate();

// And back, for the picker: the day that moment falls on in Vienna.
const shown = DateTime.fromJSDate(closesAt, { zone: 'Europe/Vienna' }).toISODate();
```

With a [TimeInput](TimeInput.md) next to it, which holds `'HH:MM'`, the two make one moment. In the user's own zone:

```ts
const start = dayTimeToDate(day, time); // an Invalid Date unless both are complete
const [shownDay, shownTime] = [dateToDay(start), dateToTime(start)]; // and back, for the two inputs
```

On the day the clocks go forward a time can be missing (02:30 in Vienna on the last Sunday of March); `dayTimeToDate` moves it on by the gap, to 03:30. On the day they go back a time happens twice, and it takes the first. In a given zone the same comes from Luxon: ``DateTime.fromISO(`${day}T${time}`, { zone })``.

## Migration from Date values

Before 0.39 every day was a `Date` at midnight UTC. Now it is a `'YYYY-MM-DD'` string, and TypeScript points at each place to change, because a `Date` no longer fits any of the props. Two things still get through: a `Date` in plain JavaScript or through a validator typed as `FormValidator<any>`, and a stored value read back as its JSON, `'2026-09-15T00:00:00.000Z'`, which is a string to TypeScript. DatePicker shows either as an empty field, logs a warning naming the prop, and leaves the value alone until the user actually edits the field.

| Before | After |
|--------|-------|
| `useState<Date \| null>(null)` | `useState<string \| null>(null)` |
| `new FormValidator<Date \| null>(null)` | `new FormValidator<string \| null>(null)` |
| `selected={new Date()}`, `minDate={new Date()}` | `selected={dateToDay(new Date())}` |
| `a.getTime() < b.getTime()` | `a < b` |
| `date.toLocaleDateString(locale)` | `dayToDate(day).toLocaleDateString(locale)` |
| `JSON.stringify(date)`, `date.toISOString()` | the string itself |

Data you already stored from DatePicker was midnight UTC of the picked day. `stored.toISOString().slice(0, 10)` turns exactly that back into the day, since the value was built in UTC; do this only for values DatePicker produced.

Where the value was really a moment (the poll and deadline cases above), decide the time zone once, in your own code, as shown in the section before this one, rather than relying on what DatePicker happened to hand out.
