Monet
@etched/monet 0.0.1

Date range picker

Ported

A 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.

Onsite window

Weekdays before the December break.

Leave (optional)
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, Space or ArrowDown opens the calendar, with the date picker's keys. The first Enter on a day starts the range; the second ends it and closes.
  • Escape closes the calendar and keeps the old range. Focus goes back to the field.
  • The trigger is a combobox with aria-haspopup="dialog". The calendar is a dialog, named by the trigger's aria-label or "Choose dates"; each month is a grid.
  • Presets are buttons with aria-pressed, true for the one that matches the range. Picked days are gridcells with aria-selected.
  • Without label, give the trigger an aria-label.

API reference

PropTypeDefaultDescription
labelReactNode–Visible label. Without one, give the control an aria-label.
labelTooltipReactNode–More about the field, behind an info button after the label.
labelTooltipLabelstring"More information"Names the info button.
descriptionReactNode–Help text below the control. Hidden while an error shows.
errorFieldError–
minDate–The earliest day that can be picked. The calendar does not go to earlier months.
maxDate–The latest day that can be picked. The calendar does not go to later months.
disabledDaysMatcher | Matcher[]–Days that cannot be picked: dates, { before: date }, { after: date }, { dayOfWeek: [0, 6] }, { from, to } or a function.
localestringthe browser's languageA BCP 47 language tag for the field text, month and day names, and the first day of the week.
formatIntl.DateTimeFormatOptions{ dateStyle: "medium" }How the field shows dates, as Intl.DateTimeFormat options.
placeholderReactNode–Shown while no date is chosen.
size"sm" | "base""base"Same heights as Input.
requiredboolean–A form does not submit while no date is chosen; the trigger takes focus.
readOnlyboolean–Shows the value, but does not open.
openboolean–Whether the calendar is open, when the app controls it.
defaultOpenbooleanfalseWhether the calendar is open at first.
onOpenChange(open: boolean) => void–Called when the calendar opens or closes.
calendarLabelstring"Choose a date", or "Choose dates" for a rangeNames the calendar's dialog when the trigger has no aria-label.
previousMonthLabelstring"Previous month"Names the calendar's previous month button.
nextMonthLabelstring"Next month"Names the calendar's next month button.
containerBasePopover.Portal.Props["container"]document.bodyThe element the calendar's portal goes in, for example inside a dialog with its own stacking.
valueDateRangeValue | null–
defaultValueDateRangeValue | null–
onValueChange(value: DateRangeValue | null) => void–
presetsDateRangePreset[]–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.
namestring–Submits the range with a form, as an ISO 8601 interval: yyyy-mm-dd/yyyy-mm-dd.