Calendar
Date grid for selection.
Install the peer
Every date component needs @internationalized/date, which is a peerDependency — install it yourself.
Dates are DateValue objects, not JavaScript Dates. You construct them, so there has to be exactly one copy of that library — two copies means objects that stop lining up, and
that is the kind of bug that takes an afternoon to find.
Usage
Root hands you months and weekdays through a snippet
and you render the table. Bits owns everything hard: the roving grid, arrow keys across weeks and
months, PageUp/PageDown, Home/End, and the calendar arithmetic itself.
value is the selection and placeholder is the month on screen — both bindable, and they are genuinely
separate: paging through months must not change what is selected.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
28 | 29 | 30 | 31 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
The 20th is marked unavailable — struck through, not merely greyed out.
Naming the calendar
Pass calendarLabel. Bits builds the root's accessible name as `${calendarLabel} ${month} ${year}` and defaults the first part to
the literal word "Event" — so an unset calendar announces as "Event January 2026",
which tells the user nothing about what they are picking.
Pass "Departure date", "Due date", "Booking". Verified in the rendered DOM and asserted in the tests, because the prop name does not make this obvious.
The nav buttons are the opposite case: Bits hardcodes their aria-label to "Previous" and "Next" and merges its own props last, so
an aria-label you pass is discarded. Do not waste time trying.
Disabled vs unavailable
These are different answers and they are styled differently on purpose:
isDateDisabled/minValue/maxValue→ out of range. Faded out; there is nothing to reason about.isDateUnavailable→ exists but taken. A fully booked day. Kept readable and struck through, which reads as "that date is gone" rather than "that date does not exist".
Collapsing both into one grey blur throws away information the user needs. data-today gets a ring rather than a fill, so today stays legible even when
it is also the selected day.
Locale is not cosmetic
locale changes the month names, which day the week starts on, and the numeral system. Hardcoding Monday-first
or Sunday-first is a correctness bug for half your users, not a styling preference. Pass the
user's locale.
Add MonthSelect and YearSelect for any range wider
than a few months. Paging one month at a time to reach a birth year is not navigation.
Props
Calendar.Root
| Prop | Type | Default |
|---|---|---|
type required `single` selects one date, `multiple` selects several. Required, because it also decides the shape of `value`. | 'single' | 'multiple' | — |
value Selected date, or dates in `multiple` mode. Bindable. A `DateValue` from `@internationalized/date`, not a JS `Date`. | DateValue | DateValue[] | undefined | — |
onValueChange Called when the selection changes. | (value: DateValue | DateValue[] | undefined) => void | — |
placeholder The month currently on screen. Bindable. | DateValue | — |
minValue Earliest selectable date. | DateValue | — |
maxValue Latest selectable date. | DateValue | — |
isDateUnavailable Return true for dates that exist but cannot be picked — a booked day. | (date: DateValue) => boolean | — |
isDateDisabled Return true for dates to disable outright. | (date: DateValue) => boolean | — |
numberOfMonths How many months to show at once. | number | 1 |
locale BCP-47 locale driving month names, weekday order and numerals. | string | 'en' |
calendarLabel What this calendar is FOR. Bits builds the root's accessible name as `${calendarLabel} ${month} ${year}`, and its default is the literal word "Event" — so an unset calendar announces as "Event January 2026", which tells the user nothing. Pass "Departure date", "Booking", "Due date". | string | 'Event' |
disabled | boolean | false |
readonly | boolean | false |
monthFormat Show the month and year dropdowns in the header. | 'long' | 'short' | 'narrow' | 'numeric' | '2-digit' | false |
class Extra classes merged onto the root. | string | — |
children | Snippet<[SnippetProps]> | — |
Every other part takes class plus its native attributes. Grid, GridHead, GridBody, GridRow, HeadCell, Header, Heading, the nav buttons and the selects are shared with Range Calendar.