Date range picker
PortedA field that shows a range of days and opens a calendar to pick one: the first day picked starts the range, the second ends it and closes the calendar. Escape closes it without a change. Picking the chosen range again, by its days or its preset, clears it, unless a range is required. Native button attributes and ref pass through to the trigger.
import { DateRangePicker, type DateRangeValue } from "@etched/monet/date-range-picker";
// Fixed dates in 2026, so the examples look the same every day.
const day = (month: number, date: number) => new Date(2026, month - 1, date);
const october: DateRangeValue = { from: day(10, 1), to: day(10, 26) };
export function Preview() {
return <DateRangePicker aria-label="Period" defaultValue={october} />;
}Installation
import { DateRangePicker } from "@etched/monet/date-range-picker";Examples
Basics
Pick the first day, then the last: the second pick closes the calendar.
import { DateRangePicker, type DateRangeValue } from "@etched/monet/date-range-picker";
// Fixed dates in 2026, so the examples look the same every day.
const day = (month: number, date: number) => new Date(2026, month - 1, date);
const october: DateRangeValue = { from: day(10, 1), to: day(10, 26) };
export function Basics() {
return (
<div className="row">
<DateRangePicker aria-label="Period" />
<DateRangePicker aria-label="Period, chosen" defaultValue={october} />
<DateRangePicker aria-label="Period, small" size="sm" defaultValue={october} />
<DateRangePicker aria-label="Period, read-only" defaultValue={october} readOnly />
<DateRangePicker aria-label="Period, disabled" defaultValue={october} disabled />
</div>
);
}Presets
Quick choices beside the calendar. The one that matches the value shows as pressed. With max at today, a preset that ends later is disabled.
Applied
import { useState } from "react";
import { DateRangePicker, type DateRangePreset, type DateRangeValue } from "@etched/monet/date-range-picker";
// Presets count back from today (Kumo's set).
const daysAgo = (days: number) => {
const date = new Date();
date.setHours(0, 0, 0, 0);
date.setDate(date.getDate() - days);
return date;
};
const today = daysAgo(0);
const presets: DateRangePreset[] = [
{ label: "Today", range: { from: today, to: today } },
{ label: "Last 7 days", range: { from: daysAgo(6), to: today } },
{ label: "Last 30 days", range: { from: daysAgo(29), to: today } },
{ label: "Last 90 days", range: { from: daysAgo(89), to: today } },
{ label: "This month", range: { from: new Date(today.getFullYear(), today.getMonth(), 1), to: new Date(today.getFullYear(), today.getMonth() + 1, 0) } },
{ label: "Last month", range: { from: new Date(today.getFullYear(), today.getMonth() - 1, 1), to: new Date(today.getFullYear(), today.getMonth(), 0) } },
];
export function Presets() {
const [range, setRange] = useState<DateRangeValue | null>(null);
return (
<div className="grid">
<DateRangePicker label="Applied" value={range} onValueChange={setRange} presets={presets} max={today} placeholder="Any time" />
</div>
);
}In a form
min and max bound the days. In Form, onFormSubmit gets the range by name as yyyy-mm-dd/yyyy-mm-dd. Reset puts the defaultValue back.
import { useState } from "react";
import { Button } from "@etched/monet/button";
import { Form } from "@etched/monet/form";
import { DateRangePicker } from "@etched/monet/date-range-picker";
// Fixed dates in 2026, so the examples look the same every day.
const day = (month: number, date: number) => new Date(2026, month - 1, date);
export function InAForm() {
const [sent, setSent] = useState("");
return (
<Form
className="grid"
onFormSubmit={(values) => setSent(Object.entries(values).map(([name, value]) => `${name}=${String(value)}`).join(", "))}
onReset={() => setSent("")}
>
<DateRangePicker
label="Onsite window"
name="window"
defaultValue={{ from: day(11, 9), to: day(11, 13) }}
min={day(10, 26)}
max={day(12, 18)}
disabledDays={{ dayOfWeek: [0, 6] }}
description="Weekdays before the December break."
required
/>
<DateRangePicker label="Leave" name="leave" locale="en-GB" format={{ day: "numeric", month: "long" }} required={false} />
<div className="row">
<Button type="submit" variant="primary">
Book
</Button>
<Button type="reset">Reset</Button>
{sent && <span>Sent: {sent}</span>}
</div>
</Form>
);
}Accessibility
Enter,SpaceorArrowDownopens the calendar, with the date picker's keys. The firstEnteron a day starts the range; the second ends it and closes.Escapecloses the calendar and keeps the old range. Focus goes back to the field.- The trigger is a
comboboxwitharia-haspopup="dialog". The calendar is adialog, named by the trigger'saria-labelor "Choose dates"; each month is agrid. - Presets are buttons with
aria-pressed,truefor the one that matches the range. Picked days aregridcells witharia-selected. - Without
label, give the trigger anaria-label.
API reference
| Prop | Type | Default | Description |
|---|---|---|---|
| label | ReactNode | – | Visible label. Without one, give the control an aria-label. |
| labelTooltip | ReactNode | – | More about the field, behind an info button after the label. |
| labelTooltipLabel | string | "More information" | Names the info button. |
| description | ReactNode | – | Help text below the control. Hidden while an error shows. |
| error | FieldError | – | |
| min | Date | – | The earliest day that can be picked. The calendar does not go to earlier months. |
| max | Date | – | The latest day that can be picked. The calendar does not go to later months. |
| disabledDays | Matcher | Matcher[] | – | Days that cannot be picked: dates, { before: date }, { after: date }, { dayOfWeek: [0, 6] }, { from, to } or a function. |
| locale | string | the browser's language | A BCP 47 language tag for the field text, month and day names, and the first day of the week. |
| format | Intl.DateTimeFormatOptions | { dateStyle: "medium" } | How the field shows dates, as Intl.DateTimeFormat options. |
| placeholder | ReactNode | – | Shown while no date is chosen. |
| size | "sm" | "base" | "base" | Same heights as Input. |
| required | boolean | – | A form does not submit while no date is chosen; the trigger takes focus. |
| readOnly | boolean | – | Shows the value, but does not open. |
| open | boolean | – | Whether the calendar is open, when the app controls it. |
| defaultOpen | boolean | false | Whether the calendar is open at first. |
| onOpenChange | (open: boolean) => void | – | Called when the calendar opens or closes. |
| calendarLabel | string | "Choose a date", or "Choose dates" for a range | Names the calendar's dialog when the trigger has no aria-label. |
| previousMonthLabel | string | "Previous month" | Names the calendar's previous month button. |
| nextMonthLabel | string | "Next month" | Names the calendar's next month button. |
| container | BasePopover.Portal.Props["container"] | document.body | The element the calendar's portal goes in, for example inside a dialog with its own stacking. |
| value | DateRangeValue | null | – | |
| defaultValue | DateRangeValue | null | – | |
| onValueChange | (value: DateRangeValue | null) => void | – | |
| presets | DateRangePreset[] | – | Quick choices beside the calendar, such as "Last 7 days". The one that matches the value shows as pressed; one that starts or ends on a day that cannot be picked is disabled. |
| name | string | – | Submits the range with a form, as an ISO 8601 interval: yyyy-mm-dd/yyyy-mm-dd. |