Components
Calendar
A bare calendar built on react-day-picker for selecting a single day, multiple days, or a date range, with keyboard navigation and dropdown month and year controls.
This is the primitive. If you want a dd/MM/yyyy text input, a popover, and presets like “Today” or “Last 7 days” already wired up, use the Tier 2 composites DatePicker and DateRangePicker instead.
Basic usage
Use mode="single" for a one-day picker. The calendar manages its own selection state when left uncontrolled; leave mode off entirely and it renders read-only.
Range selection
Set mode="range" and render two months with numberOfMonths for booking and reporting date pickers.
Multiple days
With mode="multiple" users can toggle any number of individual, non-contiguous days.
Dropdown caption
Pass captionLayout="dropdown" to swap the static month label for month and year dropdowns — handy for jumping across distant dates like a birthdate. Bound the year list with startMonth / endMonth, and note the month abbreviations default to Vietnamese — see below.
Localisation
Weekday and month names come from the locale you pass through to react-day-picker. The package version also uses locale.code to abbreviate the month dropdown, falling back to 'vi-VN' rather than the runtime's own locale. That fallback is deliberate: the vendored copy asked for toLocaleString('default'), which resolves against whatever locale the process is running under — so the server and the browser could produce different strings and React would report a hydration mismatch. A fixed code is wrong in one language; a floating one is broken in every language.
import { Calendar } from '@comitor/ui'
import { enUS, vi } from 'date-fns/locale'
// Weekday and month names come from the date-fns locale you pass through.
// The component also reads locale.code to abbreviate the month dropdown —
// it falls back to 'vi-VN', never to the runtime's own locale, because
// "default" resolves differently on the server and in the browser and that
// mismatch shows up as a hydration error rather than a wrong word.
export function Example() {
return (
<div className="flex flex-wrap gap-4">
<Calendar mode="single" locale={enUS} captionLayout="dropdown" className="rounded-md border" />
<Calendar mode="single" locale={vi} captionLayout="dropdown" className="rounded-md border" />
</div>
)
}Disabled dates
The disabled prop accepts a matcher — here every day before June 17 is non-selectable and dimmed.
Content inside a day
CalendarDayButton is exported so you can add a second line to a day — an event count, a price, a status dot — without rebuilding the cell. Swap components.DayButton for a wrapper that renders the day number plus your own <span>; the component styles that span at text-xs and opacity-90.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
mode | 'single' | 'multiple' | 'range' | — | Selection behavior — a single day, an array of days, or a contiguous date range. Leave it off for a read-only calendar. |
selected | Date | Date[] | DateRange | — | Controlled selected value. Pair with onSelect; omit for an uncontrolled calendar. The shape follows mode. |
onSelect | (value) => void | — | Called when the selection changes. Shape matches the active mode. |
defaultMonth | Date | current month | The month displayed on first render when uncontrolled. |
month / onMonthChange | Date / (month: Date) => void | — | Controls which month is displayed. Use both to drive navigation from outside the calendar. |
startMonth / endMonth | Date | — | Bounds for navigation, and the range the year dropdown offers under captionLayout="dropdown". |
numberOfMonths | number | 1 | How many months to render side by side. Useful for range pickers. |
captionLayout | 'label' | 'dropdown' | 'dropdown-months' | 'dropdown-years' | 'label' | Renders the month/year as a static label or as navigable dropdowns. |
buttonVariant | 'default' | 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive' | 'link' | 'ghost' | Button variant applied to the previous/next navigation buttons. |
showOutsideDays | boolean | true | Show days from adjacent months that fill the leading and trailing weeks. |
disabled | Matcher | Matcher[] | — | Days that cannot be selected — a date, range, predicate, or matcher object. |
locale | Locale (date-fns) | — | Weekday and month names. Its code also drives the abbreviated month dropdown, which falls back to vi-VN when no locale is given. |
formatters | Partial<Formatters> | — | Per-slot label formatters. Merged over the component’s own formatMonthDropdown, so overriding one leaves the rest intact. |
components | Partial<CustomComponents> | — | Swap individual internals. Merged over the component’s defaults — pass DayButton to build on CalendarDayButton rather than replacing it. |
className | string | — | Additional Tailwind classes merged onto the calendar root. |
classNames | Partial<ClassNames> | — | Override individual element class names (day, weekday, nav, etc.). |
All other react-day-picker props are forwarded to the underlying DayPicker.
Accessibility
- The grid follows the WAI-ARIA date picker pattern — days are reachable with arrow keys, and Enter or Space selects the focused day.
- Previous and next navigation render as real
<button>elements with accessible labels, and takearia-disabledat the ends of the navigable range. A disabled day instead gets the nativedisabledattribute, so it drops out of the tab order; style it fromdata-disabledon the cell. - The day cell carries
aria-selected, and the state is spelled out in the day button'saria-label— today reads “Today, …” and a chosen day ends in “, selected” — so neither state rests on colour alone. react-day-picker emits noaria-current; today reaches CSS asdata-today. - Focused days receive a visible
focus-visiblering using the brand ring token, and focus is automatically moved to the focused day when navigating months. - A selected day is a control in its on state, so it uses the control pair —
bg-control-onwithtext-control-check— the same pair as a checkedCheckbox. Splitting the pair works by accident in the default palette, where the two tokens happen to be equal, and breaks in the high-contrast one. - Extra content you render inside a day is capped at
opacity-90. The day stays clickable and focusable, so it is not exempt from the 4.5:1 text threshold. The worst case is a selected day in the light high-contrast palette:opacity-80lands at 4.43:1 and fails, 85% clears at 4.79:1, and the shipped 90% leaves headroom at 5.15:1. - Known limitation: the today and range middle tints both use
--accent, which sits near 1.1:1 against the calendar background. They read as a soft cue, not as information — pair them with a real signal if the state has to be perceivable on its own.