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.
import { DateTimeRangePicker } from "@umriss-ui/core";
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.
A clock change is named for each end separately.
Click two days, untick All day, type both times, then Apply; the footer counts the duration as you go.
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>
);
}
Set withSeconds where the span is measured to the second; presets={[]} drops the column when no span repeats.
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>
);
}
size="sm" suits dense forms; errors come from the FormField, and disabled locks field and panel.
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>
);
}
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.
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>
);
}
| 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. |
DateTimeRangePickerProps| Name | Type | Default | Description |
|---|---|---|---|
valuerequired | DateRange | null | — | The time span with times at both ends; null means none.Shown in: Configurator, A downtime window, With seconds and 3 more |
withSeconds | boolean | false | Show the seconds fields. Shown in: Configurator, With seconds |
placeholder | string | — | What stands in the empty field. Shown in: States |
disabled | boolean | false | Locks the field and the panel. Shown in: Configurator, 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.Shown in: Configurator, With an error |
size | "sm" | "md" | the size of a ControlSizeProvider around it, else "md" | sm for a toolbar and dense forms, md otherwise.Shown in: Configurator, 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. Shown in: Configurator |
clearable | boolean | false | Shows a cross where a value is set, which fades in on hover/focus and
clears the value. Shown in: Configurator, A downtime window, States |
presets | 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. Shown in: With seconds |
ref | Ref<HTMLSpanElement> | — | Receives the component's root element. from RefPropShown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more |
| Name | Type | Default | Description |
|---|---|---|---|
onChangerequired | (range: 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.Shown in: Configurator, A downtime window, With seconds and 3 more |
Also takes every attribute of <span> – without defaultValue, onChange.
DateRangeA span between two dates, both ends included - the value of
DateRangePicker and DateTimeRangePicker.
| Name | Type | Default | Description |
|---|---|---|---|
fromrequired | Date | — | Shown in: A downtime window, With seconds, States and 6 more |
torequired | Date | — | Shown in: A downtime window, With seconds, States and 6 more |
RangePresetA 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.
| Name | Type | Default | Description |
|---|---|---|---|
labelrequired | string | — | Shown in: Presets (DateRangePicker) |
rangerequired | () => DateRange | — | Shown in: Presets (DateRangePicker) |
What umriss deliberately does not build, and why: ADR-0032.