Skip to main content

Date & time pickers

Calendar for dates; range and time pickers with the same controlled pattern.

Quick start
import { DatePicker } from "mates-ui";

DatePicker({ value: null, label: "Date", onChange: () => {} });

DatePicker — basic

import { html, atom, renderApp } from 'mates';
import { DatePicker, Text } from 'mates-ui';

const App = () => {
const today = new Date();
const val = atom(null);
const vy = atom(today.getFullYear());
const vm = atom(today.getMonth());
return () => html`
<x-col gap="var(--md-space-3)" style="max-width:17.5rem;width:100%;">
${DatePicker({ value: val(), label: "Select a date", viewYear: vy(), viewMonth: vm(), onChange: (ts) => val.set(ts), onViewChange: (y, m) => { vy.set(y); vm.set(m); } })}
${val() ? Text("Selected: " + new Date(val()).toLocaleDateString("default", { dateStyle: "full" }), { type: "body-sm", color: "muted" }) : html``}
</x-col>
`;
};

renderApp(App, document.getElementById('app'));

DatePicker — pre-filled value

import { html, atom, renderApp } from 'mates';
import { DatePicker, Text } from 'mates-ui';

const App = () => {
const today = new Date();
const todayTs = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime();
const val = atom(todayTs);
const vy = atom(today.getFullYear());
const vm = atom(today.getMonth());
return () => html`
<x-col gap="var(--md-space-3)" style="max-width:17.5rem;width:100%;">
${DatePicker({ value: val(), label: "Date of birth", viewYear: vy(), viewMonth: vm(), onChange: (ts) => val.set(ts), onViewChange: (y, m) => { vy.set(y); vm.set(m); } })}
${val() ? Text("Selected: " + new Date(val()).toLocaleDateString(), { type: "body-sm", color: "muted" }) : html``}
</x-col>
`;
};

renderApp(App, document.getElementById('app'));

DatePicker — disabled ranges

import { html, atom, renderApp } from 'mates';
import { DatePicker, Text } from 'mates-ui';

const App = () => {
const today = new Date();
const y = today.getFullYear(); const m = today.getMonth();
const minTs = new Date(y, m - 1, 1).getTime();
const maxTs = new Date(y, m + 2, 0).getTime();
const val = atom(null);
const vy = atom(y);
const vm = atom(m);
return () => html`
<x-col gap="var(--md-space-3)" style="max-width:17.5rem;width:100%;">
${Text("Only current ±2 months selectable.", { type: "body-sm", color: "muted" })}
${DatePicker({ value: val(), label: "Booking date", min: minTs, max: maxTs, viewYear: vy(), viewMonth: vm(), onChange: (ts) => val.set(ts), onViewChange: (yy, mm) => { vy.set(yy); vm.set(mm); } })}
${val() ? Text("Selected: " + new Date(val()).toLocaleDateString(), { type: "body-sm", color: "muted" }) : html``}
</x-col>
`;
};

renderApp(App, document.getElementById('app'));

DatePicker — locale

import { html, atom, renderApp } from 'mates';
import { DatePicker } from 'mates-ui';

const App = () => {
const today = new Date();
const valDe = atom(null); const vyDe = atom(today.getFullYear()); const vmDe = atom(today.getMonth());
const valMon = atom(null); const vyMon = atom(today.getFullYear()); const vmMon = atom(today.getMonth());
return () => html`
<x-row gap="var(--md-space-5)" wrap align="flex-start">
<div style="max-width:17.5rem;">
${DatePicker({ value: valDe(), label: "Datum wählen", locale: "de", strings: { clear: "Zurücksetzen", today: "Heute", previousMonth: "Vorheriger Monat", nextMonth: "Nächster Monat", monthSelect: "Monat", yearInput: "Jahr", calendarDialog: "Kalender" }, viewYear: vyDe(), viewMonth: vmDe(), onChange: (ts) => valDe.set(ts), onViewChange: (y, m) => { vyDe.set(y); vmDe.set(m); } })}
</div>
<div style="max-width:17.5rem;">
${DatePicker({ value: valMon(), label: "Event date (Mon start)", weekStart: 1, viewYear: vyMon(), viewMonth: vmMon(), onChange: (ts) => valMon.set(ts), onViewChange: (y, m) => { vyMon.set(y); vmMon.set(m); } })}
</div>
</x-row>
`;
};

renderApp(App, document.getElementById('app'));

DatePicker — per-label props

import { html, atom, renderApp } from 'mates';
import { DatePicker } from 'mates-ui';

const App = () => {
const today = new Date();
const val = atom(null);
const vy = atom(today.getFullYear());
const vm = atom(today.getMonth());
return () => html`
<x-col gap="var(--md-space-3)" style="max-width:17.5rem;width:100%;">
${DatePicker({ value: val(), label: "Event date", previousMonthLabel: "Zurück", monthSelectLabel: "Monat", yearLabel: "Jahr", nextMonthLabel: "Weiter", calendarLabel: "Kalender", clearLabel: "Zurücksetzen", todayLabel: "Heute", viewYear: vy(), viewMonth: vm(), onChange: (ts) => val.set(ts), onViewChange: (y, m) => { vy.set(y); vm.set(m); } })}
</x-col>
`;
};

renderApp(App, document.getElementById('app'));

DateRangePicker

import { html, atom, renderApp } from 'mates';
import { DateRangePicker, Text } from 'mates-ui';

const App = () => {
const today = new Date();
const range = atom({ start: null, end: null });
const vy = atom(today.getFullYear()); const vm = atom(today.getMonth());
return () => html`
<x-col gap="var(--md-space-3)" style="max-width:17.5rem;width:100%;">
${DateRangePicker({ value: range(), label: "Booking window", viewYear: vy(), viewMonth: vm(), onChange: (r) => range.set(r), onViewChange: (y, m) => { vy.set(y); vm.set(m); } })}
${range().start && range().end
? Text(new Date(Math.min(range().start, range().end)).toLocaleDateString() + " → " + new Date(Math.max(range().start, range().end)).toLocaleDateString(), { type: "body-sm", color: "muted" })
: range().start ? Text("End date pending…", { type: "body-sm", color: "muted" }) : html``}
</x-col>
`;
};

renderApp(App, document.getElementById('app'));

DatePicker — multiple selection

import { html, atom, renderApp } from 'mates';
import { DatePicker, Text } from 'mates-ui';

const App = () => {
const today = new Date();
const values = atom([]);
const vy = atom(today.getFullYear()); const vm = atom(today.getMonth());
return () => html`
<x-col gap="var(--md-space-3)" style="max-width:20rem;width:100%;">
${DatePicker({ multiple: true, values: values(), label: "Unavailable dates", viewYear: vy(), viewMonth: vm(), onChangeMultiple: (ts) => values.set(ts), onViewChange: (y, m) => { vy.set(y); vm.set(m); } })}
${values().length > 0 ? Text(values().length + " date(s) selected", { type: "body-sm", color: "muted" }) : html``}
</x-col>
`;
};

renderApp(App, document.getElementById('app'));

DateRangePicker — multiple ranges (no overlap)

import { html, atom, renderApp } from 'mates';
import { DateRangePicker, Text } from 'mates-ui';

const App = () => {
const today = new Date();
const ranges = atom([]);
const inProgress = atom({ start: null, end: null });
const vy = atom(today.getFullYear()); const vm = atom(today.getMonth());
return () => html`
<x-col gap="var(--md-space-3)" style="max-width:20rem;width:100%;">
${Text("Overlapping ranges are blocked.", { type: "body-sm", color: "muted" })}
${DateRangePicker({ multiple: true, allowOverlappingRanges: false, values: ranges(), value: inProgress(), label: "Blocked periods", viewYear: vy(), viewMonth: vm(), onChangeMultiple: (r) => ranges.set(r), onChange: (r) => inProgress.set(r), onViewChange: (y, m) => { vy.set(y); vm.set(m); } })}
${ranges().length > 0 ? Text(ranges().length + " range(s) — no overlaps", { type: "body-sm", color: "muted" }) : html``}
</x-col>
`;
};

renderApp(App, document.getElementById('app'));

DateRangePicker — multiple ranges (overlapping allowed)

import { html, atom, renderApp } from 'mates';
import { DateRangePicker, Text } from 'mates-ui';

const App = () => {
const today = new Date();
const ranges = atom([]);
const inProgress = atom({ start: null, end: null });
const vy = atom(today.getFullYear()); const vm = atom(today.getMonth());
return () => html`
<x-col gap="var(--md-space-3)" style="max-width:20rem;width:100%;">
${Text("Overlapping ranges are permitted.", { type: "body-sm", color: "muted" })}
${DateRangePicker({ multiple: true, allowOverlappingRanges: true, values: ranges(), value: inProgress(), label: "Availability windows", viewYear: vy(), viewMonth: vm(), onChangeMultiple: (r) => ranges.set(r), onChange: (r) => inProgress.set(r), onViewChange: (y, m) => { vy.set(y); vm.set(m); } })}
${ranges().length > 0 ? Text(ranges().length + " range(s) — overlaps OK", { type: "body-sm", color: "muted" }) : html``}
</x-col>
`;
};

renderApp(App, document.getElementById('app'));

TimePicker

import { html, atom, renderApp } from 'mates';
import { TimePicker, Text } from 'mates-ui';

const App = () => {
const time = atom(null);
return () => html`
<x-col gap="var(--md-space-3)" style="max-width:17.5rem;width:100%;">
${TimePicker({ value: time(), label: "Start time", onChange: (v) => time.set(v) })}
${time() ? Text("Value (24 h): " + time().hour + ":" + String(time().minute).padStart(2, "0"), { type: "body-sm", color: "muted" }) : html``}
</x-col>
`;
};

renderApp(App, document.getElementById('app'));

DateTimePicker

import { html, atom, renderApp } from 'mates';
import { DateTimePicker, Text } from 'mates-ui';

const App = () => {
const today = new Date();
const val = atom(null);
const vy = atom(today.getFullYear()); const vm = atom(today.getMonth());
return () => html`
<x-col gap="var(--md-space-3)" style="max-width:20rem;width:100%;">
${DateTimePicker({ value: val(), label: "Appointment", minuteStep: 5, viewYear: vy(), viewMonth: vm(), onChange: (ts) => val.set(ts), onViewChange: (y, m) => { vy.set(y); vm.set(m); } })}
${val() ? Text("ISO: " + new Date(val()).toISOString(), { type: "body-sm", color: "muted" }) : html``}
</x-col>
`;
};

renderApp(App, document.getElementById('app'));

DatePicker / DateRangePicker / TimePicker / DateTimePicker

import { DatePicker, DateRangePicker, TimePicker, DateTimePicker } from "mates-ui";
Prop Type Default Description
attr AttrMap Extra HTML attributes applied to the root element.
calendarLabel string "Calendar" Aria-label for the day grid.
classes string | string[] Extra root classes — joined and appended after built-in classes.
clearLabel string "Clear" Text of the Clear footer button.
data DataAttrMap Root `data-*` map (`testId` → `data-testid`). `testId` prop / `attr` win over `data`.
disabled boolean false Disables the entire picker — the calendar cannot be opened.
disabledRanges DateDisabledRange[] Array of { start, end } timestamp pairs — both ends inclusive — marking non-selectable dates.
id string Root `id` shorthand (`attr.id` wins). Form native-control `id` props are unchanged.
label string Visible label rendered above the trigger input.
locale string BCP-47 locale tag used to localise month names and weekday headers. Defaults to the browser locale.
max number Latest selectable date as a Unix ms timestamp. Dates after this are disabled.
maxVisibleChips number 3 Maximum chips shown in the trigger before +N overflow badge.
min number Earliest selectable date as a Unix ms timestamp. Dates before this are disabled.
monthSelectLabel string "Month" Aria-label for the month dropdown.
multiple boolean false Enable multi-date selection. Use values / onChangeMultiple instead of value / onChange.
nextMonthLabel string "Next month" Aria-label for the next-month chevron.
onChange (v: number | null) => void Called with a Unix ms timestamp (or null) whenever the selection changes.
onChangeMultiple (ts: number[]) => void Fires when the multi-selection changes.
onViewChange (year: number, month: number) => void Called when the user navigates the calendar to a different month/year.
previousMonthLabel string "Previous month" Aria-label for the previous-month chevron.
selectedDatesLabel string "Selected dates" Title of the "+N" overflow dialog in multiple mode.
strings Partial<DatePickerUiStrings> Override UI strings such as Clear, Today, and ARIA labels. Newer per-label props below take precedence when both are set.
todayLabel string "Today" Text of the Today footer button.
value* number | null Selected date as a Unix millisecond timestamp, or null when empty.
values (number | string)[] Controlled array of selected timestamps (multi mode only).
viewMonth number Controlled 1-based month of the visible calendar page.
viewYear number Controlled year of the visible calendar page.
weekStart 0 | 1 0 First day of the week. 0 = Sunday, 1 = Monday.
yearLabel string "Year" Aria-label for the year dropdown.

DateRangePicker

Prop Type Default Description
calendarLabel string "Calendar" Day grid aria-label.
clearLabel string "Clear" Clear button text.
label string Visible label above the trigger input.
monthSelectLabel string "Month" Month dropdown aria-label.
nextMonthLabel string "Next month" Next-month chevron aria-label.
onChange (r: DateRange) => void Called after each click — first click sets start, second sets end.
onViewChange (year: number, month: number) => void Called when the user navigates the calendar.
previousMonthLabel string "Previous month" Previous-month chevron aria-label.
selectedRangesLabel string "Selected ranges" Overflow dialog title in multiple mode.
todayLabel string "Today" Today button text.
value* DateRange Controlled range object { start: number | null; end: number | null }.
viewMonth number Controlled 1-based month of the visible calendar page.
viewYear number Controlled year of the visible calendar page.
yearLabel string "Year" Year dropdown aria-label.

TimePicker

Prop Type Default Description
clearLabel string "Clear" Clear button text.
dialogLabel string "Time picker" Accessible name of the time picker dialog.
disabled boolean false Disables the time picker.
hourLabel string "Hour" Hour dropdown label (visible text + aria-label).
label string Visible label above the picker.
minuteLabel string "Minute" Minute dropdown label (visible text + aria-label).
minuteStep number 1 Interval between selectable minute values.
okLabel string "OK" OK button text.
onChange (v: TimeValue | null) => void Called with a 24-hour TimeValue when the selection changes.
value* TimeValue | null Controlled time as { hour: number; minute: number } in 24-hour format, or null.

DateTimePicker

Prop Type Default Description
clearLabel string "Clear" Clear button text.
dialogLabel string "Date and time" Accessible name of the date-time dialog.
hourLabel string "Hour" Hour dropdown label.
label string Visible label above the combined picker.
minuteLabel string "Minute" Minute dropdown label.
minuteStep number 1 Interval between selectable minute values in the time portion.
monthSelectLabel string "Month" Month dropdown aria-label.
nextMonthLabel string "Next month" Next-month chevron aria-label.
onChange (v: number | null) => void Called with a full Unix ms timestamp. Picking only a date preserves the current time (or uses now).
onViewChange (year: number, month: number) => void Called when the user navigates the calendar.
previousMonthLabel string "Previous month" Previous-month chevron aria-label.
timeLabel string "Time" Time section heading.
todayLabel string "Today" Today button text.
value* number | null Controlled value as a Unix ms timestamp representing both date and time.
viewMonth number Controlled 1-based month of the visible calendar page.
viewYear number Controlled year of the visible calendar page.
yearLabel string "Year" Year dropdown aria-label.

CSS tokens

Token Role
--md-color-border Trigger input and calendar border.
--md-color-primary Selected day background and range fill.
--md-color-primary-muted In-range day highlight background.
--md-color-surface Calendar popover background.
--md-color-surface-raised Today indicator background.
--md-radius-md Border radius of the calendar popover.

Caveats

  • `value` is a Unix millisecond timestamp (Date.now() style) — not a Date object.
  • Locale defaults to the browser locale when omitted.
  • `disabledRanges` accepts `{ start, end }` timestamp pairs — both ends are inclusive.
  • Per-label `xxxLabel` props (e.g. `clearLabel`, `yearLabel`) take precedence over the `strings` object when both are supplied.