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.
import { DateRangePicker } from "@umriss-ui/core";
Both ends lie at local midnight and both count. The value contract of the pickers stands on DatePicker.
The same { from, to } shape is what @umriss-ui/table reads a date filter from.
Two clicks set both ends, in either order; the value is { from, to }, both inclusive, at local midnight.
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>
);
}
Pass presets for the spans your users pick most; an empty list hides the column, and none gives the defaults.
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>
);
}
size="sm" suits dense forms; errors come from the FormField, and disabled locks field and calendar.
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>
);
}
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 { 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>
);
}
| 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. |
DateRangePickerProps| Name | Type | Default | Description |
|---|---|---|---|
valuerequired | DateRange | null | — | The time span, both ends on local midnight and both inclusive; null
means none.Shown in: Configurator, A span of days, Presets and 3 more |
placeholder | string | — | What stands in the empty field. Shown in: Presets |
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 span of days, Presets and 2 more |
presets | RangePreset[] | DEFAULT_PRESETS, in the wording's labels | The quick-select column in the panel. An empty array hides the column. Shown in: Presets |
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 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.Shown in: Configurator, A span of days, Presets 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 span of days, Presets, States and 6 more |
torequired | Date | — | Shown in: A span of days, Presets, States and 6 more |
What umriss deliberately does not build, and why: ADR-0032.