# DatePicker

> Package `@umriss-ui/core`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/core/datepicker/>. Every page in one line: [llms.txt](https://romanhaendler.github.io/umriss-ui/core/llms.txt); every page in full: [llms-full.txt](https://romanhaendler.github.io/umriss-ui/core/llms-full.txt).

One day, picked in a calendar (date field): a delivery date, a due date. Weeks begin on Monday.

```ts
import { DatePicker } from "@umriss-ui/core";
```

Demo page: https://romanhaendler.github.io/umriss-ui/core/datepicker/

The value contract of the four pickers stands here. `DatePicker` hands out a `Date` at local midnight on every path and reports as soon as the day is chosen. [DateTimePicker](https://romanhaendler.github.io/umriss-ui/core/datetimepicker/) hands out an instant and reports only on Apply, Now or Clear. [DateRangePicker](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/) gives both ends at local midnight, both inclusive. [DateTimeRangePicker](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/) sets an all-day end to the end of the day, not to its midnight.

`null` means no date, never 1 January 1970. A range never comes out backwards: ends picked in reverse are swapped, so there is no error state to handle. A time that falls into a clock change is named in the panel, never guessed: a skipped hour moves forward, and a doubled hour asks which of the two.

## Examples

### Pick a day

The value is a `Date` at local midnight or `null`; `onChange` runs as soon as a day is chosen.

```tsx
import { useState } from "react";
import { DatePicker, FormField, Stack, Text, useFormats } from "@umriss-ui/core";

export default function PickADay() {
  const formats = useFormats();
  const [delivery, setDelivery] = useState<Date | null>(null);

  return (
    <Stack gap={3} style={{ maxWidth: 320 }}>
      <FormField label="Delivery date">
        <DatePicker value={delivery} onChange={setDelivery} clearable />
      </FormField>
      <Text size="xs" tone="muted" mono>
        value: {delivery === null ? "null" : formats.dateTime(delivery, true)}
      </Text>
    </Stack>
  );
}
```

### States

`size="sm"` suits dense forms; errors come from the `FormField`, and `disabled` locks field and calendar.

```tsx
import { useState } from "react";
import { DatePicker, FormField, Grid } from "@umriss-ui/core";

export default function States() {
  const [pickUp, setPickUp] = useState<Date | null>(new Date(2026, 2, 18));
  const [delivery, setDelivery] = useState<Date | null>(new Date(2026, 2, 16));

  return (
    <Grid minItemWidth="220px" gap={4}>
      <FormField label="Pick-up" hint="Small size.">
        <DatePicker size="sm" value={pickUp} onChange={setPickUp} clearable />
      </FormField>
      <FormField label="Delivery" error="The delivery date cannot lie before today.">
        <DatePicker value={delivery} onChange={setDelivery} clearable />
      </FormField>
      <FormField label="Delivered on" hint="Set by the driver's scan.">
        <DatePicker value={null} onChange={() => {}} disabled />
      </FormField>
      <FormField label="Return by" hint="Empty, with its own placeholder.">
        <DatePicker value={null} onChange={() => {}} placeholder="No return planned" />
      </FormField>
    </Grid>
  );
}
```

### With an error

A `FormField` with an `error` marks the field invalid by itself. Without one, set `invalid` and tie your message to the field with `aria-describedby`.

```tsx
import { useId, useState } from "react";
import { DatePicker, Stack, Text } from "@umriss-ui/core";

export default function WithAnError() {
  const [delivery, setDelivery] = useState<Date | null>(new Date(2026, 2, 16));
  const messageId = useId();
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const error = delivery !== null && delivery < today ? "The delivery date cannot lie before today." : undefined;

  return (
    <Stack gap={1} style={{ maxWidth: 280 }}>
      <DatePicker
        aria-label="Delivery"
        value={delivery}
        onChange={setDelivery}
        invalid={error !== undefined}
        aria-describedby={error ? messageId : undefined}
      />
      {error && (
        <Text id={messageId} size="xs" style={{ color: "var(--u-color-danger-text)" }}>
          {error}
        </Text>
      )}
    </Stack>
  );
}
```

### Check a due date

Compare two pickers' values as plain dates, since both lie at local midnight, and put the result in the `FormField`.

```tsx
import { useState } from "react";
import { DatePicker, FormField, Grid } from "@umriss-ui/core";

const DAY = 24 * 60 * 60 * 1000;

export default function CheckADueDate() {
  const [issued, setIssued] = useState<Date | null>(new Date(2026, 2, 2));
  const [due, setDue] = useState<Date | null>(new Date(2026, 1, 27));

  const term = issued && due ? Math.round((due.getTime() - issued.getTime()) / DAY) : null;
  const error = term !== null && term < 0 ? "The due date lies before the invoice date." : undefined;
  const hint = term !== null && term >= 0 ? `Payment term: ${term} days.` : undefined;

  return (
    <Grid minItemWidth="220px" gap={4} style={{ maxWidth: 520 }}>
      <FormField label="Invoice date">
        <DatePicker value={issued} onChange={setIssued} />
      </FormField>
      <FormField label="Due" hint={hint} error={error}>
        <DatePicker value={due} onChange={setDue} />
      </FormField>
    </Grid>
  );
}
```

## When to use something else

- A day and a time of day → DateTimePicker
- A span of days → DateRangePicker
- A span with times at both ends → DateTimeRangePicker

## Keyboard

| Key | Action |
|---|---|
| `Enter or Space` | On the field, opens the calendar; on a day, chooses it. |
| `Arrow keys` | Move a day left or right, a week up or down. |
| `Escape` | Closes the calendar and returns to the field. |

## API

### `DatePickerProps`

| Prop | Type | Default | Description |
|---|---|---|---|
| `value` *required* | `Date \| null` | — | The day, as a `Date` on local midnight; `null` means none.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/datepicker/#configurator-datepicker), [Pick a day](https://romanhaendler.github.io/umriss-ui/core/datepicker/#pick-a-day), [States](https://romanhaendler.github.io/umriss-ui/core/datepicker/#states) and 8 more |
| `placeholder` | `string` | — | What stands in the empty field.<br>Shown in: [States](https://romanhaendler.github.io/umriss-ui/core/datepicker/#states) |
| `disabled` | `boolean` | `false` | Locks the field and the panel.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/datepicker/#configurator-datepicker), [States](https://romanhaendler.github.io/umriss-ui/core/datepicker/#states) |
| `invalid` | `boolean` | — | Marks the field as invalid. `FormField` sets it itself as soon as it carries an `error` - by hand only necessary without `FormField`.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/datepicker/#configurator-datepicker), [With an error](https://romanhaendler.github.io/umriss-ui/core/datepicker/#with-an-error) |
| `size` | `"sm" \| "md"` | the size of a `ControlSizeProvider` around it, else `"md"` | `sm` for a toolbar and dense forms, `md` otherwise.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/datepicker/#configurator-datepicker), [States](https://romanhaendler.github.io/umriss-ui/core/datepicker/#states) |
| `chars` | `number` | — | The width in characters - room for the value; the field adds its own padding, glyph and cross. Given, the field is that wide wherever it stands, and never wider than its place. Without it the field fills its place, and where the place asks - in a toolbar or a row - it is as wide as its longest value in the formats in use, so a date never truncates.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/datepicker/#configurator-datepicker) |
| `clearable` | `boolean` | `false` | Shows a cross where a value is set, which fades in on hover/focus and clears the value.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/datepicker/#configurator-datepicker), [Pick a day](https://romanhaendler.github.io/umriss-ui/core/datepicker/#pick-a-day), [States](https://romanhaendler.github.io/umriss-ui/core/datepicker/#states) and 4 more |
| `ref` | `Ref<HTMLSpanElement>` | — | Receives the component's root element. From `RefProp`.<br>Shown in: [A row that holds still (Sizes)](https://romanhaendler.github.io/umriss-ui/core/sizes/#a-row-that-holds-still), [An anchored surface (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#anchored-surface), [Take the width of the trigger (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#width-from-anchor) and 2 more |

#### Events

| Prop | Type | Default | Description |
|---|---|---|---|
| `onChange` *required* | `(date: Date \| null) => void` | — | Runs as soon as a day is settled - on a click in the grid, on "Today" and on clearing (`null`). Every one of these ways hands out local midnight; there is none that delivers a time along with it.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/datepicker/#configurator-datepicker), [Pick a day](https://romanhaendler.github.io/umriss-ui/core/datepicker/#pick-a-day), [States](https://romanhaendler.github.io/umriss-ui/core/datepicker/#states) and 8 more |

Also takes every attribute of `<span>` – without `defaultValue`, `onChange`.

## Known limits

- No typing of a date into the field.
- umriss shows the reader's time ([ADR-0060](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0060-validation-and-a-layout-editor-are-never-time-zones-not-yet.md)).

What umriss deliberately does not build, and why: [ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/).
