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.

June 2026

Range selection

Set mode="range" and render two months with numberOfMonths for booking and reporting date pickers.

June 2026
July 2026

Multiple days

With mode="multiple" users can toggle any number of individual, non-contiguous days.

June 2026

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.

June 2026

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.

tsx
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.

June 2026

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.

June 2026

Props

PropTypeDefaultDescription
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.
selectedDate | Date[] | DateRangeControlled selected value. Pair with onSelect; omit for an uncontrolled calendar. The shape follows mode.
onSelect(value) => voidCalled when the selection changes. Shape matches the active mode.
defaultMonthDatecurrent monthThe month displayed on first render when uncontrolled.
month / onMonthChangeDate / (month: Date) => voidControls which month is displayed. Use both to drive navigation from outside the calendar.
startMonth / endMonthDateBounds for navigation, and the range the year dropdown offers under captionLayout="dropdown".
numberOfMonthsnumber1How 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.
showOutsideDaysbooleantrueShow days from adjacent months that fill the leading and trailing weeks.
disabledMatcher | Matcher[]Days that cannot be selected — a date, range, predicate, or matcher object.
localeLocale (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.
formattersPartial<Formatters>Per-slot label formatters. Merged over the component’s own formatMonthDropdown, so overriding one leaves the rest intact.
componentsPartial<CustomComponents>Swap individual internals. Merged over the component’s defaults — pass DayButton to build on CalendarDayButton rather than replacing it.
classNamestringAdditional Tailwind classes merged onto the calendar root.
classNamesPartial<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 take aria-disabled at the ends of the navigable range. A disabled day instead gets the native disabled attribute, so it drops out of the tab order; style it from data-disabled on the cell.
  • The day cell carries aria-selected, and the state is spelled out in the day button's aria-label — today reads “Today, …” and a chosen day ends in “, selected” — so neither state rests on colour alone. react-day-picker emits no aria-current; today reaches CSS as data-today.
  • Focused days receive a visible focus-visible ring 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-on with text-control-check — the same pair as a checked Checkbox. 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-80 lands 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.