Monet
@etched/monet 0.0.1

Date picker

Ported

A month grid to pick one day, several days or a range (mode), on react-day-picker. Arrow keys move by day and week, Page Up and Page Down by month (with Shift, by year), Home and End to the week's ends. Several months hide the days of other months, so no day shows twice.

Kumo source
import { DatePicker } from "@etched/monet/date-picker";

// Fixed dates in 2026, so the examples look the same every day. Open calendars show past months, so "today" is never in them.
const day = (month: number, date: number) => new Date(2026, month - 1, date);

export function Preview() {
  return <DatePicker aria-label="Review date" defaultValue={day(10, 26)} />;
}

Installation

import { Calendar, DatePicker } from "@etched/monet/date-picker";

Examples

Basics

import { DatePicker } from "@etched/monet/date-picker";

// Fixed dates in 2026, so the examples look the same every day. Open calendars show past months, so "today" is never in them.
const day = (month: number, date: number) => new Date(2026, month - 1, date);

export function Basics() {
  return (
    <div className="row">
      <DatePicker aria-label="Due date" />
      <DatePicker aria-label="Review date" defaultValue={day(10, 26)} />
      <DatePicker aria-label="Review date, small" size="sm" defaultValue={day(10, 26)} />
      <DatePicker aria-label="Review date, read-only" defaultValue={day(10, 26)} readOnly />
      <DatePicker aria-label="Review date, disabled" defaultValue={day(10, 26)} disabled />
    </div>
  );
}

In a form

In Form, a picker is a field like any other. required stops a submit while no date is chosen and focuses the picker; the server's error for its name shows under it; onFormSubmit gets the date as yyyy-mm-dd. min and max bound the days and the months. Reset puts each defaultValue back; a disabled date is not sent. Here the server refuses November 26.

Start date

Between October 26 and the end of the year.

Offer expires (optional)
Onsite date
Signed on
import { useState } from "react";
import { Button } from "@etched/monet/button";
import { Form, type FormErrors } from "@etched/monet/form";
import { DatePicker } from "@etched/monet/date-picker";

// Fixed dates in 2026, so the examples look the same every day. Open calendars show past months, so "today" is never in them.
const day = (month: number, date: number) => new Date(2026, month - 1, date);

export function InAForm() {
  const [sent, setSent] = useState("");
  const [errors, setErrors] = useState<FormErrors>();

  return (
    <Form
      className="grid"
      errors={errors}
      onFormSubmit={(values) => {
        const refused = values.onsite === "2026-11-26";
        setErrors(refused ? { onsite: "Kai Ito is not in the office that day." } : {});
        setSent(refused ? "" : Object.entries(values).map(([name, value]) => `${name}=${String(value)}`).join(", "));
      }}
      onReset={() => setSent("")}
    >
      <DatePicker label="Start date" name="start" defaultValue={day(11, 2)} min={day(10, 26)} max={day(12, 31)} description="Between October 26 and the end of the year." />
      <DatePicker label="Offer expires" name="expires" required={false} />
      <DatePicker label="Onsite date" name="onsite" required min={day(11, 2)} max={day(11, 30)} />
      <DatePicker label="Signed on" name="signed" defaultValue={day(10, 19)} disabled />
      <div className="row">
        <Button type="submit" variant="primary">
          Send invite
        </Button>
        <Button type="reset">Reset</Button>
        {sent && <span>Sent: {sent}</span>}
      </div>
    </Form>
  );
}

Disabled days

Weekends and two holidays cannot be picked; arrow keys skip them.

Interview date
import { DatePicker } from "@etched/monet/date-picker";

// Fixed dates in 2026, so the examples look the same every day. Open calendars show past months, so "today" is never in them.
const day = (month: number, date: number) => new Date(2026, month - 1, date);

export function DisabledDays() {
  return (
    <div className="grid">
      <DatePicker label="Interview date" defaultValue={day(11, 24)} disabledDays={[{ dayOfWeek: [0, 6] }, day(11, 26), day(11, 27)]} />
    </div>
  );
}

Locale

Month and day names, the date in the field and the first day of the week follow locale.

import { DatePicker } from "@etched/monet/date-picker";

// Fixed dates in 2026, so the examples look the same every day. Open calendars show past months, so "today" is never in them.
const day = (month: number, date: number) => new Date(2026, month - 1, date);

export function Locale() {
  return (
    <div className="row">
      <DatePicker aria-label="Date, German" locale="de-DE" defaultValue={day(10, 26)} />
      <DatePicker aria-label="Date, British" locale="en-GB" defaultValue={day(10, 26)} format={{ dateStyle: "long" }} />
      <DatePicker aria-label="Date, Japanese" locale="ja-JP" defaultValue={day(10, 26)} />
    </div>
  );
}

Calendar

The month grid on its own, for a panel or a filter.

March 2026
import { useState } from "react";
import { Calendar } from "@etched/monet/date-picker";

// Fixed dates in 2026, so the examples look the same every day. Open calendars show past months, so "today" is never in them.
const day = (month: number, date: number) => new Date(2026, month - 1, date);

export function CalendarAlone() {
  const [date, setDate] = useState<Date | null>(day(3, 18));

  return (
    <Calendar
      mode="single"
      value={date}
      onValueChange={(next) => setDate(next ?? null)}
      defaultMonth={day(3, 1)}
      footer={date ? `Chosen: ${date.toLocaleDateString("en-US", { dateStyle: "long" })}` : "No date chosen"}
    />
  );
}

Several days

mode="multiple" with max. Grey days are taken.

March 2026
import { useState } from "react";
import { Calendar } from "@etched/monet/date-picker";

// Fixed dates in 2026, so the examples look the same every day. Open calendars show past months, so "today" is never in them.
const day = (month: number, date: number) => new Date(2026, month - 1, date);

export function SeveralDays() {
  const [dates, setDates] = useState<Date[]>([day(3, 10), day(3, 11)]);

  return (
    <Calendar
      mode="multiple"
      value={dates}
      onValueChange={(next) => setDates(next ?? [])}
      max={3}
      defaultMonth={day(3, 1)}
      disabled={[day(3, 5), day(3, 12), day(3, 19)]}
      fixedWeeks
      footer={`${dates.length} of 3 days chosen.`}
    />
  );
}

Month and year lists

captionLayout="dropdown" for dates far away; startMonth and endMonth bound the lists.

June 2026
import { Calendar } from "@etched/monet/date-picker";

// Fixed dates in 2026, so the examples look the same every day. Open calendars show past months, so "today" is never in them.
const day = (month: number, date: number) => new Date(2026, month - 1, date);

export function MonthAndYearLists() {
  return <Calendar mode="single" captionLayout="dropdown" defaultMonth={day(6, 1)} startMonth={day(1, 1)} endMonth={day(12, 1)} />;
}

Accessibility

  • Enter, Space or ArrowDown opens the calendar on the chosen day, or today. Arrow keys move by day and week and skip disabled days.
  • PageUp/PageDown move by month (with Shift, by year), and Home/End to the week's ends. Enter picks and closes; Escape closes with no change.
  • The trigger is a combobox with aria-haspopup="dialog", named by label or aria-label. The calendar is a dialog, named by the trigger's aria-label, or by calendarLabel ("Choose a date").
  • Each month is a grid named by the month. Each day is named in full, such as "Monday, November 2, 2026, selected", with "Today" first for today.
  • Without label, give the trigger an aria-label. Set locale to the page's language, so screen readers hear the month and day names in it.
  • The month buttons are named "Previous month" and "Next month"; pass translated names with previousMonthLabel and nextMonthLabel.

API reference

Calendar

PropTypeDefaultDescription
localestringthe browser's languageA BCP 47 language tag for month and day names, the labels screen readers hear and the first day of the week.
valueMode["selected"] | null–The chosen day, days or range, when the app controls it.
defaultValueMode["selected"] | null–The first chosen day, days or range, when the calendar keeps its own.
onValueChangeMode["onSelect"]–Called with the new value, the day that was picked, its modifiers and the event (react-day-picker's onSelect).

DatePicker

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.
namestring–Submits the value with a form, as yyyy-mm-dd.
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.
valueDate | null–
defaultValueDate | null–
onValueChange(value: Date | null) => void–