Calendar
A month grid for picking one date, several dates or a range, with keyboard navigation across months.
Single date
Click a day or move with the arrows and press Enter. The footer is a live region that announces the pick.
value: 2026-10-14
Code
<Calendar value={state.date} footer={picked(state.date)}
help="Arrow keys move between days. Enter picks one." />Usage
import { Calendar } from "@elements/ui/calendar";
<Calendar value={form.due} />
<Calendar mode="multiple" values={form.days} max={5} />
<Calendar mode="range" from={trip.from} to={trip.to} numberOfMonths={2} />
<Calendar captionLayout="dropdown" startMonth="1930-01" disabled={{ after: today }} />Examples
Required
required keeps a selection: the single date cannot be cleared, the last of several dates stays, and a range keeps its start.
value: 2026-10-20
values: 2026-10-08; range: 2026-10-19 to none
Code
<Calendar value={form.date} required />
<Calendar mode="multiple" values={form.days} required />
<Calendar mode="range" from={trip.from} to={trip.to} required />Range
Two months side by side. The first click starts the range, the second ends it, in either order.
2026-10-12 to 2026-10-23
Code
<Calendar mode="range" from={trip.from} to={trip.to} numberOfMonths={2} />Multiple
Toggle up to five days, never fewer than one. A sixth starts a new selection.
2026-10-06, 2026-10-15
Code
<Calendar mode="multiple" values={state.days} min={1} max={5} />Month and year dropdowns
For a date of birth: pick the year and month from selects instead of paging. The selects are native, so phones show their own pickers.
value: 1990-06-15
Code
<Calendar captionLayout="dropdown" value={person.dob}
startMonth="1930-01" endMonth="2026-12" reverseYears />
<Calendar captionLayout="dropdown-months" ... />
<Calendar captionLayout="dropdown-years" ... />Custom select dropdowns
dropdownStyle=listbox draws the month and year dropdowns as the library's Select (a combobox with a listbox) instead of native selects.
Code
<Calendar captionLayout="dropdown" dropdownStyle="listbox" value={state.picked}
footer={state.picked ? `Selected: ${formatDate(state.picked, "en-US")}` : "Pick a day."} />Intercepting clicks
onDayClick with preventDefault replaces the pick: a double click selects the day, a single click clears it, and the footer shows the date. Enter still selects from the keyboard.
Code
<Calendar value={state.picked} footer={dateString(state.picked)}
onDayClick={(d, m, e) => onDay(state, d, e)} />
function onDay(state, iso, e) {
e.preventDefault(); // skip the default pick
state.picked = e.detail >= 2 || e.detail === 0 ? iso : ""; // double click (or Enter) picks; a click clears
}Disabled and booked dates
Past days, weekends and booked days are disabled; the booked ones are marked through a custom modifier.
value: none
Code
<Calendar value={state.booking}
disabled={[{ before: today }, { dayOfWeek: [0, 6] }, BOOKED]}
modifiers={{ booked: BOOKED }} />
/* page.css: strike the booked days */
.calendar-day[data-modifiers~="booked"] > .calendar-day-button { text-decoration: line-through; }Range rules
A stay of 2 to 7 nights that cannot cross a weekend (excludeDisabled), and a range that restarts on the next click (resetOnSelect).
stay: none to none; reset: 2026-10-05 to 2026-10-09
Code
<!-- 2 to 7 nights, never across a weekend -->
<Calendar mode="range" from={stay.from} to={stay.to}
min={2} max={7} disabled={{ dayOfWeek: [0, 6] }} excludeDisabled />
<!-- a click after a complete range starts a new one -->
<Calendar mode="range" from={r.from} to={r.to} resetOnSelect />Week numbers
The locale's week numbers with six fixed weeks, ISO 8601 weeks, the US broadcast calendar, and Persian weeks counted from 1 Farvardin.
Code
<Calendar showWeekNumber fixedWeeks />
<Calendar showWeekNumber isoWeek />
<Calendar showWeekNumber broadcastCalendar />
<!-- weeks of the calendar's own year: week 1 holds 1 Farvardin -->
<Calendar showWeekNumber calendar="persian" locale="fa-IR" dir="rtl" numerals="latn" />Time zones
timeZone sets which day is today: in Kiritimati (UTC+14) and Pago Pago (UTC-11) it is never the same day. Dates are plain ISO days, so January 1900 in Kolkata, whose old offset has seconds, still starts on Monday.
Code
<!-- today is the date in the zone: these two are a day apart -->
<Calendar timeZone="Pacific/Kiritimati" />
<Calendar timeZone="Pacific/Pago_Pago" />
<!-- dates are plain ISO days, so an old zone offset never shifts a day -->
<Calendar timeZone="Asia/Kolkata" defaultMonth="1900-01" />Outside and hidden days
Without outside days the other months' cells stay empty (this one starts on Monday with weekStartsOn). hidden removes days entirely; the keyboard skips them.
Code
<Calendar showOutsideDays={false} weekStartsOn={1} />
<Calendar hidden={{ dayOfWeek: [0, 6] }} />Several months
pagedNavigation moves two months at a time; reverseMonths shows the later month first.
Code
<Calendar mode="range" numberOfMonths={2} pagedNavigation />
<Calendar numberOfMonths={2} reverseMonths />Navigation limits
startMonth and endMonth bound the buttons, the keyboard and the dropdowns; the older names fromMonth, toMonth, fromYear and toYear do too. disableNavigation keeps one month; hideNavigation removes the buttons.
Code
<Calendar startMonth="2026-09" endMonth="2026-12" />
<Calendar disableNavigation />
<Calendar hideNavigation captionLayout="dropdown" />
<!-- the older bound names, still read: -->
<Calendar fromMonth="2026-09" toMonth="2026-12" />
<Calendar captionLayout="dropdown" fromYear={2024} toYear={2027} />Navigation after the caption
navLayout=after puts both buttons after the caption, so the Tab order matches what you see.
Code
<Calendar navLayout="after" captionLayout="dropdown" />Locales and right to left
Names, the first day of the week and week numbering come from Intl. Arabic reads right to left; Hindi uses Devanagari digits.
Code
<Calendar locale="ar-EG" dir="rtl" />
<Calendar locale="de-DE" showWeekNumber />
<Calendar locale="hi-IN" numerals="deva" />Other calendars
calendar sets the Intl calendar the grid follows: Persian, Hijri, Buddhist, Ethiopic or Hebrew months, captions and day numbers, each in its language and with Latin digits (in English, Amharic for Ethiopic), and the dropdowns list the calendar's own months and years. The value stays a Gregorian ISO date.
value: none
Code
<Calendar calendar="persian" locale="fa-IR" dir="rtl" value={state.date} />
<Calendar calendar="persian" locale="en-US" numerals="latn" value={state.date} />
<Calendar calendar="islamic-umalqura" locale="ar-SA" dir="rtl" numerals="arab" />
<Calendar calendar="buddhist" locale="th-TH" numerals="thai" />
<Calendar calendar="ethiopic" locale="am-ET" numerals="ethi" /> <!-- Ge'ez numerals; numerals="latn" for Latin -->
<Calendar calendar="hebrew" locale="he-IL" dir="rtl" />
<Calendar calendar="hebrew" locale="en-US" captionLayout="dropdown" /> <!-- Adar I and Adar II in a leap year -->Presets
Quick picks beside the calendar set the date and show its month.
Code
<Calendar value={state.date} month={state.month} />
<Button e:for={p of PRESETS} onclick={() => setPreset(state, p.days)}>{p.label}</Button>Controlled month
month is two-way. Bind it to show a month from code, such as a Go to Today button.
Code
<Calendar value={state.date} month={state.month} />
<Button onclick={() => state.month = today}>Go to Today</Button>Cell size
Set --calendar-cell-size for larger cells; everything else follows.
Larger cells
Code
<Calendar class="calendar-large" />
/* page.css */
.calendar-large { --calendar-cell-size: var(--control-height-lg); }Day content
dayContent puts a second line in each day: here a nightly price, higher on weekends and none on outside days, in larger cells.
Code
<Calendar mode="range" from={trip.from} to={trip.to} captionLayout="dropdown"
class="calendar-priced" dayContent={price} />
function price(date: ISODate, m: DayModifiers): string {
if (m.outside) return "";
let day = new Date(date + "T00:00:00").getDay();
return day === 0 || day === 6 ? "$120" : "$100";
}Events
Every interaction reports itself; the last five events are shown.
Code
<Calendar value={state.date}
onSelect={(date, m) => log(`select ${date}`)}
onMonthChange={(month) => log(`month ${month}`)}
onDayFocus={(date) => log(`focus ${date}`)} />Matchers
Every kind of rule disabled at once: a date, a range, before, after, between, and a function.
Code
<Calendar disabled={[
"2026-10-20", // one date
{ from: "2026-10-05", to: "2026-10-07" }, // a range, ends included
{ before: "2026-10-03" }, // before, not including
{ after: "2026-10-27" }, // after, not including
{ before: "2026-10-24", after: "2026-10-21" }, // between
(date) => date.endsWith("-13"), // your own rule
]} />Focus on show
autoFocus moves focus to the calendar when it appears, for a calendar revealed by an action.
Code
<Calendar e:if={state.open} autoFocus />Display
mode none only reports clicks. hideWeekdays drops the header row, formatters change the caption, day and weekday text, and firstWeekContainsDate sets which week of January is week 1 (here the week holding January 1). role and label name the whole calendar.
clicked: none
Code
<Calendar mode="none" onDayClick={(date) => opened = date} />
<Calendar hideWeekdays />
<Calendar formatters={{
caption: (month) => month.slice(0, 7), // "2026-10"
day: (date) => date.slice(8), // "01"
weekday: (date, locale) => narrowWeekday(date, locale),
}} />
<Calendar showWeekNumber weekStartsOn={1} firstWeekContainsDate={1}
role="group" label="Week planner" />Change events
onValuesChange and onRangeChange report the new selection after each pick.
no change yet
Code
<Calendar mode="multiple" values={days}
onValuesChange={(values) => save(values)} />
<Calendar mode="range" from={trip.from} to={trip.to}
onRangeChange={(range) => save(range)} />Year buttons
yearButtons adds previous and next year buttons beside the month buttons, as the WAI-ARIA date picker dialog has. They respect startMonth and endMonth.
no year move yet
Code
<Calendar value={form.date} yearButtons
onPrevYearClick={(m) => log(m)} onNextYearClick={(m) => log(m)} />Custom selections
mode none with onDayClick and modifiers builds any selection: a whole week, a rolling seven day window or a full month. weekNumberButtons makes each week number a button; onWeekNumberClick selects that week.
none
Code
<Calendar mode="none" showWeekNumber weekNumberButtons
modifiers={{ selected: { from, to }, rangeStart: from, rangeEnd: to,
rangeMiddle: { after: from, before: to } }}
onDayClick={(date) => pick(date)}
onWeekNumberClick={(week, dates) => pickWeek(dates)} />Translated labels
labels sets every accessible name and formatters every visible text, here in Spanish: the navigation, the dropdowns, the week numbers, the weekday headers, the days and the grid.
Code
<Calendar locale="es" captionLayout="dropdown" showWeekNumber yearButtons navLayout="after"
labels={{ previous: "Ir al mes anterior", next: "Ir al mes siguiente",
weekday: (date) => ..., day: (date, m) => ..., grid: (month) => ... }}
formatters={{ weekNumberHeader: () => "#", weekNumber: (n) => `S${n}`,
monthDropdown: (m) => ..., yearDropdown: (y) => ... }} />Animated months
animate slides the weeks toward the direction of travel; reduced motion turns it off.
Code
<Calendar animate />