# DateTimePicker

> Package `@umriss-ui/core`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/core/datetimepicker/>. 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 moment, day and time of day in one field (date-time field): when an incident began, when an alert fired.

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

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

It reports only on Apply, Now or Clear, never with a day that has no time yet. The value contract of the pickers stands on [DatePicker](https://romanhaendler.github.io/umriss-ui/core/datepicker/).

Around a clock change the panel names a skipped hour and asks which of a doubled hour is meant, for example 02:30 on 25 October 2026.

## Examples

### Set an instant

Choose the day, type the time, then Apply; `onChange` runs only then, never with a day that has no time.

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

export default function SetAnInstant() {
  const formats = useFormats();
  const [started, setStarted] = useState<Date | null>(null);

  return (
    <Stack gap={3} style={{ maxWidth: 320 }}>
      <FormField label="Customer impact began">
        <DateTimePicker value={started} onChange={setStarted} clearable />
      </FormField>
      <Text size="xs" tone="muted" mono>
        value: {started === null ? "null" : `${formats.dateTime(started, false)} ${formats.offset(started)}`}
      </Text>
    </Stack>
  );
}
```

### With seconds

Set `withSeconds` where the second is part of the value, such as the moment an alert fired.

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

export default function WithSeconds() {
  const [fired, setFired] = useState<Date | null>(new Date(2026, 2, 17, 9, 41, 27));

  return (
    <FormField label="Alert fired" style={{ maxWidth: 320 }}>
      <DateTimePicker value={fired} onChange={setFired} withSeconds />
    </FormField>
  );
}
```

### States

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

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

export default function States() {
  const [acknowledged, setAcknowledged] = useState<Date | null>(new Date(2026, 2, 17, 9, 46));
  const [resolved, setResolved] = useState<Date | null>(new Date(2026, 2, 17, 9, 30));

  return (
    <Grid minItemWidth="240px" gap={4}>
      <FormField label="Acknowledged" hint="Small size.">
        <DateTimePicker size="sm" value={acknowledged} onChange={setAcknowledged} clearable />
      </FormField>
      <FormField label="Resolved" error="Resolved lies before the incident was opened at 09:42.">
        <DateTimePicker value={resolved} onChange={setResolved} clearable />
      </FormField>
      <FormField label="Closed" hint="Empty, with its own placeholder.">
        <DateTimePicker value={null} onChange={() => {}} placeholder="Still open" />
      </FormField>
      <FormField label="Opened" hint="Set by the alert.">
        <DateTimePicker value={new Date(2026, 2, 17, 9, 42)} 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 { DateTimePicker, Stack, Text } from "@umriss-ui/core";

/* INC-1048 was opened by the alert at 09:42. */
const OPENED = new Date(2026, 2, 17, 9, 42);

export default function WithAnError() {
  const [resolved, setResolved] = useState<Date | null>(new Date(2026, 2, 17, 9, 30));
  const messageId = useId();
  const error = resolved !== null && resolved < OPENED ? "Resolved lies before the incident was opened at 09:42." : undefined;

  return (
    <Stack gap={1} style={{ maxWidth: 320 }}>
      <DateTimePicker
        aria-label="Resolved"
        value={resolved}
        onChange={setResolved}
        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 day without a time → DatePicker
- A span with times at both ends → DateTimeRangePicker

## Keyboard

| Key | Action |
|---|---|
| `Arrow keys` | In the calendar, move a day or a week. |
| `Enter or Space` | On a day, chooses it and moves on to the hour. |
| `Arrow Up / Arrow Down` | In a time field, steps the hour, minute or second. |
| `Enter` | In a time field, applies the value. |
| `Escape` | Closes the panel without a change. |

## API

### `DateTimePickerProps`

| Prop | Type | Default | Description |
|---|---|---|---|
| `value` *required* | `Date \| null` | — | The instant; `null` means none.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/datetimepicker/#configurator-datetimepicker), [Set an instant](https://romanhaendler.github.io/umriss-ui/core/datetimepicker/#set-an-instant), [With seconds](https://romanhaendler.github.io/umriss-ui/core/datetimepicker/#with-seconds) and 3 more |
| `withSeconds` | `boolean` | `false` | Show the seconds field.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/datetimepicker/#configurator-datetimepicker), [With seconds](https://romanhaendler.github.io/umriss-ui/core/datetimepicker/#with-seconds) |
| `placeholder` | `string` | — | What stands in the empty field.<br>Shown in: [States](https://romanhaendler.github.io/umriss-ui/core/datetimepicker/#states) |
| `disabled` | `boolean` | `false` | Locks the field and the panel.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/datetimepicker/#configurator-datetimepicker), [States](https://romanhaendler.github.io/umriss-ui/core/datetimepicker/#states) |
| `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/datetimepicker/#configurator-datetimepicker), [States](https://romanhaendler.github.io/umriss-ui/core/datetimepicker/#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/datetimepicker/#configurator-datetimepicker) |
| `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/datetimepicker/#configurator-datetimepicker), [With an error](https://romanhaendler.github.io/umriss-ui/core/datetimepicker/#with-an-error) |
| `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/datetimepicker/#configurator-datetimepicker), [Set an instant](https://romanhaendler.github.io/umriss-ui/core/datetimepicker/#set-an-instant), [States](https://romanhaendler.github.io/umriss-ui/core/datetimepicker/#states) |
| `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 only on completion - "Apply", "Now" or "Clear" - and not already on the click on a day: a day without a time would be a half-set value here, and a half value is no value.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/datetimepicker/#configurator-datetimepicker), [Set an instant](https://romanhaendler.github.io/umriss-ui/core/datetimepicker/#set-an-instant), [With seconds](https://romanhaendler.github.io/umriss-ui/core/datetimepicker/#with-seconds) and 3 more |

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

## 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)).

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