# DateTimeRangePicker

> Package `@umriss-ui/core`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/>. 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 with a time at each end (date-time range): a planned downtime, a release freeze, the minutes an objective was breached. The footer shows the duration while it is set.

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

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

It starts on All day: two clicks set the days, from 00:00 to the end of the last day. Untick All day to type the times. It reports only on Apply. The value contract of the pickers stands on [DatePicker](https://romanhaendler.github.io/umriss-ui/core/datepicker/).

A clock change is named for each end separately.

## Examples

### A downtime window

Click two days, untick All day, type both times, then Apply; the footer counts the duration as you go.

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

export default function ADowntimeWindow() {
  const formats = useFormats();
  const [downtime, setDowntime] = useState<DateRange | null>(null);

  return (
    <Stack gap={3} style={{ maxWidth: 360 }}>
      <FormField label="Planned downtime, Billing">
        <DateTimeRangePicker value={downtime} onChange={setDowntime} clearable />
      </FormField>
      <Text size="xs" tone="muted" mono>
        {downtime === null
          ? "value: null"
          : `${formats.dateTime(downtime.from, false)} to ${formats.dateTime(downtime.to, false)}`}
      </Text>
    </Stack>
  );
}
```

### With seconds

Set `withSeconds` where the span is measured to the second; `presets={[]}` drops the column when no span repeats.

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

export default function WithSeconds() {
  const [breach, setBreach] = useState<DateRange | null>({
    from: new Date(2026, 2, 17, 9, 40, 12),
    to: new Date(2026, 2, 17, 10, 9, 48),
  });

  return (
    <FormField label="Objective breached, Checkout" style={{ maxWidth: 420 }}>
      <DateTimeRangePicker value={breach} onChange={setBreach} withSeconds presets={[]} />
    </FormField>
  );
}
```

### States

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

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

export default function States() {
  const [freeze, setFreeze] = useState<DateRange | null>(null);
  const [downtime, setDowntime] = useState<DateRange | null>({
    from: new Date(2026, 2, 21, 2, 0),
    to: new Date(2026, 2, 21, 6, 0),
  });

  return (
    <Grid minItemWidth="260px" gap={4}>
      <FormField label="Release freeze" hint="Small size.">
        <DateTimeRangePicker size="sm" value={freeze} onChange={setFreeze} clearable />
      </FormField>
      <FormField label="Planned downtime, Search" error="Overlaps the downtime of Checkout, 03:00 to 04:00.">
        <DateTimeRangePicker value={downtime} onChange={setDowntime} clearable />
      </FormField>
      <FormField label="Maintenance window, Payments" hint="Empty, with its own placeholder.">
        <DateTimeRangePicker value={null} onChange={() => {}} placeholder="Not scheduled yet" />
      </FormField>
      <FormField label="Downtime, last quarter" hint="Closed.">
        <DateTimeRangePicker
          value={{ from: new Date(2025, 11, 6, 1, 0), to: new Date(2025, 11, 6, 3, 30) }}
          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 { DateTimeRangePicker, Stack, Text } from "@umriss-ui/core";
import type { DateRange } from "@umriss-ui/core";

/* Checkout is down from 03:00 to 04:00 that night; Search depends on it. */
const CHECKOUT = { from: new Date(2026, 2, 21, 3, 0), to: new Date(2026, 2, 21, 4, 0) };

export default function WithAnError() {
  const [downtime, setDowntime] = useState<DateRange | null>({
    from: new Date(2026, 2, 21, 2, 0),
    to: new Date(2026, 2, 21, 6, 0),
  });
  const messageId = useId();
  const overlaps = downtime !== null && downtime.from < CHECKOUT.to && downtime.to > CHECKOUT.from;
  const error = overlaps ? "Overlaps the downtime of Checkout, 03:00 to 04:00." : undefined;

  return (
    <Stack gap={1} style={{ maxWidth: 420 }}>
      <DateTimeRangePicker
        aria-label="Planned downtime, Search"
        value={downtime}
        onChange={setDowntime}
        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

- Whole days only → DateRangePicker
- A single moment → DateTimePicker

## Keyboard

| Key | Action |
|---|---|
| `Arrow keys` | In the calendar, move a day or a week. |
| `Enter or Space` | Sets the start day, then the end day. |
| `Arrow Up / Arrow Down` | In a time field, steps its value. |
| `Enter` | In a time field, applies the span. |
| `Escape` | Closes the panel without a change. |

## API

### `DateTimeRangePickerProps`

| Prop | Type | Default | Description |
|---|---|---|---|
| `value` *required* | [`DateRange`](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#type-DateRange)` \| null` | — | The time span with times at both ends; `null` means none.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#configurator-datetimerangepicker), [A downtime window](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#a-downtime-window), [With seconds](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#with-seconds) and 3 more |
| `withSeconds` | `boolean` | `false` | Show the seconds fields.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#configurator-datetimerangepicker), [With seconds](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#with-seconds) |
| `placeholder` | `string` | — | What stands in the empty field.<br>Shown in: [States](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#states) |
| `disabled` | `boolean` | `false` | Locks the field and the panel.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#configurator-datetimerangepicker), [States](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#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/datetimerangepicker/#configurator-datetimerangepicker), [With an error](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#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/datetimerangepicker/#configurator-datetimerangepicker), [States](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#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/datetimerangepicker/#configurator-datetimerangepicker) |
| `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/datetimerangepicker/#configurator-datetimerangepicker), [A downtime window](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#a-downtime-window), [States](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#states) |
| `presets` | [`RangePreset`](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#type-RangePreset)`[]` | `DEFAULT_PRESETS`, in the wording's labels | The quick-select column in the panel; presets apply for the whole day and close immediately. An empty array hides the column.<br>Shown in: [With seconds](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#with-seconds) |
| `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/datetimerangepicker/#type-DateRange)` \| null) => void` | — | Runs only on "Apply" and on clearing (`null`), never between the two clicks. If the end lies before the start, both swap on applying - there is no error state.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#configurator-datetimerangepicker), [A downtime window](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#a-downtime-window), [With seconds](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#with-seconds) 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 downtime window](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#a-downtime-window), [With seconds](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#with-seconds), [States](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#states) and 6 more |
| `to` *required* | `Date` | — | <br>Shown in: [A downtime window](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#a-downtime-window), [With seconds](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#with-seconds), [States](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#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 (DateRangePicker)](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#presets) |
| `range` *required* | `() => `[`DateRange`](https://romanhaendler.github.io/umriss-ui/core/datetimerangepicker/#type-DateRange) | — | <br>Shown in: [Presets (DateRangePicker)](https://romanhaendler.github.io/umriss-ui/core/daterangepicker/#presets) |

## Known limits

- 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)).
- No recurring windows.

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