Calendar
Calendar.Root holds one selected date as a Temporal.PlainDate, or with mode="range" a range of dates. Calendar.Header shows the month and year between buttons to the previous and next month, and Calendar.Grid shows that month's days, one week to a row. Pressing the month and year swaps the days for a page of twelve years, and the buttons beside it go to the previous and next page. Picking a year shows its month again, keeping the month and day where it can. The root takes value with onValueChange, or defaultValue, and calls onValueChange with the date a person picks.
On this page
<Calendar.Root aria-label="Start date" value={date} onValueChange={setDate}>
<Calendar.Header />
<Calendar.Grid />
</Calendar.Root>
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|
nullFor a date in a form, use Field.DatePicker: a button in a field that opens this calendar in a popover.
Range and locale
min and max bound the dates that can be picked, the month buttons stop at their months, and years outside them are disabled. isDateDisabled rules out other dates, such as weekends or holidays. Days outside min and max are disabled buttons. A date isDateDisabled rules out can still take focus, so a screen reader can read it, but a press or Enter does not pick it.
With no value, the grid starts on defaultMonth, else on today's month, kept within min and max. Month and weekday names come from locale, else the browser's own locale. The first column is firstDayOfWeek, from 1 (Monday) to 7 (Sunday); without it, the calendar uses the locale's first day where the browser reports one through Intl.Locale, else Sunday.
A server render does not know today, so with no value or defaultMonth the grid has no weeks until the page hydrates, and today is marked only then. When the calendar renders on a server, also pass locale and firstDayOfWeek, so the server and the browser draw the same names and columns.
Ranges
With mode="range", Calendar.Root holds a DateRange, { start, end } with both days included, and value, defaultValue and onValueChange take one. The first pick starts a range and the second ends it, in either order, and onValueChange gets the range once the second date is picked; picking the same day twice makes a one-day range. Until then the grid shows the range from the first pick to the hovered day, or else the focused day, when that day can end the range, and otherwise only the first pick. Escape drops the first pick, as does a modal's close request such as the Android back gesture, and so does a change to min, max or isDateDisabled that rules it out. A range holds only days that can be picked, so after the first pick, a day past one that isDateDisabled, min or max rules out can't end it. To find that day, isDateDisabled is called for each day from the first pick to the shown month, within min and max, until it rules one out. A render checks at most about two years of those days, and the rest are checked in short slices between frames; until the scan reaches a day, that day can't end the range. This scan reuses its answers while isDateDisabled, the first pick, min and max stay the same, so a function made anew on each render scans again, and a scan that hasn't finished also starts over when any of them changes, so a far range needs a function that stays the same. One that reads changing data, such as booked days, should be made anew when that data changes, and the scan then starts again from the first pick. Apart from the scan, each day the grid draws is checked on every render. A new value equal to one of the last eight picks reported that no value has answered yet is taken as the parent's answer to that pick, however late it comes, so it neither moves the grid nor drops a range started since; that pick and the older ones then count as answered, so quick picks can be answered in turn. Any other new value drops a first pick and moves the grid to its start, unless the focused day is one of its ends. So a parent that turns a pick down and later sets that same value itself, with no other value between, doesn't move the grid to it. On the page of years, every year the range touches is selected, or while a range waits for its end, only the first pick's year.
<Calendar.Root aria-label="Stay" mode="range" value={range} onValueChange={setRange}>
<Calendar.Header />
<Calendar.Grid />
</Calendar.Root>
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
2026-10-12/2026-10-16For a range in a form, use Field.DateRangePicker.
Temporal
Todae ships no Temporal polyfill. Calendar.Root calls Temporal when it renders, so where the browser has no Temporal, the app loads a polyfill (see Native platform).
Accessible composition
Todae handles: Calendar.Root is a role="group". Calendar.Grid is a role="grid" table, labelled by the header's month while a Calendar.Header is rendered, with a column header for each weekday whose abbr is the full name. It is one Tab stop: arrow keys move by day and week, Home and End to the ends of the week, Page Up and Page Down by month, and with Shift by year. Left and Right swap in a right-to-left page. Each day is a button named with the full date, the selected one's cell has aria-selected, and today has aria-current="date". Dates isDateDisabled rules out have aria-disabled, and days outside min and max are disabled. The month and year is a button with aria-expanded, true while the years show, inside a live region, so a screen reader hears it change. The page of years is a role="grid" with the same single Tab stop: arrow keys move by year and row, Home and End to the ends of the row, and Page Up and Page Down by page. The buttons beside the heading are named from the previousMonth and nextMonth strings in StringsProvider, or previousYears and nextYears while the years show, and they and the heading draw the chevron glyph from GlyphsProvider. disabled, or a disabled Fieldset, disables every button. In range mode, the grid and the page of years have aria-multiselectable, and every day or year the range touches has aria-selected, or while a range waits for its end, only the first pick's. After the first pick, the pickEndDate string is announced through a live region, and the days that can't end the range have aria-disabled and still take focus. Escape, or a modal's close request such as the Android back gesture, then drops the first pick and stops there, so a popover or dialog around the calendar stays open.
You supply: A name for the group, with aria-label or aria-labelledby, and one for the grid when there is no Calendar.Header. locale when the page's language is not the browser's.
Watch for: Ruling dates out with isDateDisabled but not saying why. Explain the rule nearby, such as "Weekdays only".
When to use
When: Pick one date from a calendar shown on the page, such as on a booking screen.
When not: A date in a form field → Field.DatePicker. A range of dates in a form field → Field.DateRangePicker. A date with a time → Field.DateTimePicker.
Props
| Name | Type | Description |
|---|---|---|
mode | 'single' | 'range' | One date, or with "range" a range of dates: the first pick starts it and the second ends it. Default: 'single' |
value | Temporal.PlainDate | DateRange | null | Controlled selected date, or with mode="range" the selected range, or null for none. Pass with onValueChange. |
defaultValue | Temporal.PlainDate | DateRange | null | Initial selected date, or with mode="range" the initial range, when uncontrolled. |
onValueChange | ((value: Temporal.PlainDate) => void)| ((value: DateRange) => void) | Fires with each date a person picks, even the one already selected. With mode="range", fires with the range when its second date is picked. |
min | Temporal.PlainDate | The earliest date that can be picked. |
max | Temporal.PlainDate | The latest date that can be picked. |
isDateDisabled | (date: Temporal.PlainDate) => boolean | Returns true for a date that cannot be picked, such as a holiday. In range mode, a range can't hold one. |
defaultMonth | Temporal.PlainYearMonth | The month shown first when there is no value. Today's month when not set. |
firstDayOfWeek | 1 | 2 | 3 | 4 | 5 | 6 | 7 | The first column of the grid, 1 (Monday) to 7 (Sunday). The locale's own first day when the browser reports it, else 7. |
locale | string | The locale for month and weekday names, such as "ko". The browser's own when not set. |
disabled | boolean | Stops every part from changing the date or month. A disabled Fieldset sets it too. |
| Name | Type | Description |
|---|
| Name | Type | Description |
|---|