# DateRangePicker

> Package `@umriss-ui/core`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/core/daterangepicker/>. 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).

A span of days, from one day to another (date range): leave, a sprint, a reporting period. Two months side by side, presets for the usual spans, and the days counted while the pointer moves.

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

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

Both ends lie at local midnight and both count. The value contract of the pickers stands on [DatePicker](https://romanhaendler.github.io/umriss-ui/core/datepicker/).

The same `{ from, to }` shape is what `@umriss-ui/table` reads a date filter from.

## Examples

### A span of days

Two clicks set both ends, in either order; the value is `{ from, to }`, both inclusive, at local midnight.

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

export default function ASpanOfDays() {
  const formats = useFormats();
  const [leave, setLeave] = useState<DateRange | null>(null);

  return (
    <Stack gap={3} style={{ maxWidth: 340 }}>
      <FormField label="Leave">
        <DateRangePicker value={leave} onChange={setLeave} clearable />
      </FormField>
      <Text size="xs" tone="muted" mono>
        {leave === null
          ? "value: null"
          : `${formats.date(leave.from)} to ${formats.date(leave.to)}, ${rangeDays(leave.from, leave.to)} days`}
      </Text>
    </Stack>
  );
}
```

### Presets

Pass `presets` for the spans your users pick most; an empty list hides the column, and none gives the defaults.

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

const fromToday = (days: number): DateRange => {
  const today = new Date();
  const from = new Date(today.getFullYear(), today.getMonth(), today.getDate());
  return { from, to: new Date(from.getFullYear(), from.getMonth(), from.getDate() + days - 1) };
};

export default function Presets() {
  const [sprint, setSprint] = useState<DateRange | null>(null);
  const [workshop, setWorkshop] = useState<DateRange | null>(null);
  const [report, setReport] = useState<DateRange | null>(null);

  return (
    <Grid minItemWidth="240px" gap={4}>
      <FormField label="Sprint" hint="Presets of your own.">
        <DateRangePicker
          value={sprint}
          onChange={setSprint}
          clearable
          placeholder="Choose a sprint"
          presets={[
            { label: "Next 2 weeks", range: () => fromToday(14) },
            { label: "Next 3 weeks", range: () => fromToday(21) },
          ]}
        />
      </FormField>
      <FormField label="Workshop" hint="No presets.">
        <DateRangePicker value={workshop} onChange={setWorkshop} presets={[]} />
      </FormField>
      <FormField label="Report" hint="The default presets.">
        <DateRangePicker value={report} onChange={setReport} />
      </FormField>
    </Grid>
  );
}
```

### States

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

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

export default function States() {
  const [training, setTraining] = useState<DateRange | null>({ from: new Date(2026, 3, 13), to: new Date(2026, 3, 14) });
  const [leave, setLeave] = useState<DateRange | null>({ from: new Date(2026, 2, 16), to: new Date(2026, 2, 27) });

  return (
    <Grid minItemWidth="240px" gap={4}>
      <FormField label="Training" hint="Small size.">
        <DateRangePicker size="sm" value={training} onChange={setTraining} clearable />
      </FormField>
      <FormField label="Leave" error="Chloe Durand is already on leave from 23 March.">
        <DateRangePicker value={leave} onChange={setLeave} clearable />
      </FormField>
      <FormField label="Sprint 14" hint="Set by the sprint plan.">
        <DateRangePicker value={{ from: new Date(2026, 2, 9), to: new Date(2026, 2, 20) }} onChange={() => {}} disabled />
      </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 { DateRangePicker, Stack, Text } from "@umriss-ui/core";
import type { DateRange } from "@umriss-ui/core";

/* Chloe Durand covers for Noah Fischer and is away herself from 23 March. */
const AWAY_FROM = new Date(2026, 2, 23);

export default function WithAnError() {
  const [leave, setLeave] = useState<DateRange | null>({ from: new Date(2026, 2, 16), to: new Date(2026, 2, 27) });
  const messageId = useId();
  const error = leave !== null && leave.to >= AWAY_FROM ? "Chloe Durand, who covers for you, is on leave from 23 March." : undefined;

  return (
    <Stack gap={1} style={{ maxWidth: 360 }}>
      <DateRangePicker
        aria-label="Leave, Noah Fischer"
        value={leave}
        onChange={setLeave}
        invalid={error !== undefined}
        aria-describedby={error ? messageId : undefined}
      />
      {error && (
        <Text id={messageId} size="xs" style={{ color: "var(--u-color-danger-text)" }}>
          {error}
        </Text>
      )}
    </Stack>
  );
}
```

## When to use something else

- A span with times at both ends → DateTimeRangePicker
- A single day → DatePicker

## Keyboard

| Key | Action |
|---|---|
| `Arrow keys` | Move a day or a week, across both months. |
| `Enter or Space` | Sets the start, then the end. |
| `Escape` | Closes the panel. |

## API

### `DateRangePickerProps`

| Prop | Type | Default | Description |
|---|---|---|---|
| `value` *required* | [`DateRange`](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#type-DateRange)` \| null` | — | The time span, both ends on local midnight and both inclusive; `null` means none.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#configurator-daterangepicker), [A span of days](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#a-span-of-days), [Presets](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#presets) and 3 more |
| `placeholder` | `string` | — | What stands in the empty field.<br>Shown in: [Presets](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#presets) |
| `disabled` | `boolean` | `false` | Locks the field and the panel.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#configurator-daterangepicker), [States](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#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/daterangepicker/#configurator-daterangepicker), [With an error](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#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/daterangepicker/#configurator-daterangepicker), [States](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#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/daterangepicker/#configurator-daterangepicker) |
| `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/daterangepicker/#configurator-daterangepicker), [A span of days](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#a-span-of-days), [Presets](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#presets) and 2 more |
| `presets` | [`RangePreset`](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#type-RangePreset)`[]` | `DEFAULT_PRESETS`, in the wording's labels | The quick-select column in the panel. An empty array hides the column.<br>Shown in: [Presets](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#presets) |
| `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* | `(range: `[`DateRange`](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#type-DateRange)` \| null) => void` | — | Runs as soon as both ends are settled - after the second click, on a preset and on clearing (`null`). Ends dragged backwards swap silently; there is no error state to handle.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#configurator-daterangepicker), [A span of days](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#a-span-of-days), [Presets](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#presets) and 3 more |

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

### Types on this page

#### `DateRange`

A span between two dates, both ends included - the value of
`DateRangePicker` and `DateTimeRangePicker`.

| Prop | Type | Default | Description |
|---|---|---|---|
| `from` *required* | `Date` | — | <br>Shown in: [A span of days](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#a-span-of-days), [Presets](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#presets), [States](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#states) and 6 more |
| `to` *required* | `Date` | — | <br>Shown in: [A span of days](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#a-span-of-days), [Presets](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#presets), [States](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#states) and 6 more |

#### `RangePreset`

A shortcut in a range picker's panel: its label, and a function that
computes the range when it is chosen - so that "Last 7 days" is counted from
the moment of the click.

| Prop | Type | Default | Description |
|---|---|---|---|
| `label` *required* | `string` | — | <br>Shown in: [Presets](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#presets) |
| `range` *required* | `() => `[`DateRange`](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#type-DateRange) | — | <br>Shown in: [Presets](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#presets) |

## Known limits

- One span; no list of separate days.

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