Calendar
A smart, self-constraining month-grid date picker (single, multiple, or range) built on react-day-picker. Owns first-class constraints (min/max/unavailable + clamped nav), fast navigation (dropdown captions or a grid drill-down), range preview feel, a `size` axis, per-day indicator dots, and an opt-in Today footer.
@garn/ui/calendarDefault
An inline single-date calendar (controlled).
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Range
Two-month range selection with the hover/focus preview band and a 2-night minimum.
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Dropdown
Fast navigation via month + year dropdown captions, clamped to a fromYear/toYear range.
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Sizes
The size axis — sm (dense) / md / lg (airy) via --garn-calendar-cell.
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Constrained
minDate/maxDate clamping with weekends marked unavailable via isDateUnavailable.
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Indicators
Per-day indicator dots from getDayIndicators — up to four tones below the day number.
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Availability
A three-month availability view with blackout dates (isDateUnavailable).
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add calendar- Registry
calendar- Source
- calendar.tsx
- Deps
react-day-pickerdate-fnsclass-variance-authoritylucide-react- Registry deps
buttonutilsuse-controllable-state
Import
import { Calendar } from "@garn/ui/calendar";Props
Generated from the component's TypeScript types.
size- Description
- Density / airiness of the grid. Maps to `--garn-calendar-cell`.
- Type
"lg" | "md" | "sm"- Default
md
fromYear- Description
- Bound the year dropdown / navigation when no min/max is given.
- Type
number
getDayIndicators- Description
- Per-day indicator dots (planned items, notifications, warnings, errors). Return one tone per dot; up to four render below the day number.
- Type
(date: Date) => CalendarIndicatorTone[] | undefined
gridNavigation- Description
- Fast nav by drill-down: click the caption to open a year grid, then a month grid (clamped to min/max). An alternative to `captionLayout` dropdowns; forces the `label` caption.
- Type
boolean
isDateUnavailable- Description
- Per-date disabling (holidays, blackout dates).
- Type
(date: Date) => boolean
maxDate- Description
- No description.
- Type
Date
maxNights- Description
- No description.
- Type
number
minDate- Description
- Lower / upper selectable bound (inclusive, day granularity). Clamps navigation, the year dropdown, and the disabled matcher.
- Type
Date
minNights- Description
- Range mode: minimum / maximum number of nights (days) in the range.
- Type
number
showToday- Description
- Render a footer with a Today jump-and-select + an aria-live selection summary (inside rdp's `role="status"` live-region footer).
- Type
boolean
todayLabel- Description
- No description.
- Type
string
toYear- Description
- No description.
- Type
number
Plus 62 inherited native HTML attributes.
Styling
Target these data-slots and remap these tokens to restyle without forking the component.
[data-slot="calendar"][data-slot="calendar-nav-prev"][data-slot="calendar-nav-next"][data-slot="calendar-caption"][data-slot="calendar-caption-trigger"][data-slot="calendar-weekday"][data-slot="calendar-day"][data-slot="calendar-footer"][data-slot="calendar-summary"]| Property | Token | Tier |
|---|---|---|
dayCellSize | --garn-calendar-cell | semantic · air |
navButtonSize | --garn-control-h-sm | semantic · air |
panelPadding | --garn-pad-panel | semantic · air |
Overview
Calendar picks a date, several dates, or a date range from a month grid — shown inline, or dropped into a Popover to build a date field. It's the smart substrate the whole date family embeds, so its constraints and navigation live here, not re-derived per picker: minDate / maxDate / isDateUnavailable build a single disabled matcher AND clamp navigation; fast nav is month + year dropdowns by default (captionLayout="dropdown") or an opt-in gridNavigation drill-down. Range mode gets real feel — a preview band from the anchor to the pointed day on hover and keyboard focus, resetOnSelect, and minNights / maxNights. A size axis (sm / md / lg) drives the --garn-calendar-cell token; getDayIndicators paints up to four tone dots per day; an opt-in showToday footer adds a live selection summary. Use it when seeing the month in context matters (booking, scheduling, availability); when the user knows the exact date, a typed field is faster — a Calendar inside a Popover gives you both.
Guidelines
Constrain in the grid, don't validate after. Pass minDate / maxDate / isDateUnavailable rather than a hand-built disabled matcher — they clamp navigation and the year dropdown too, so out-of-range and blackout days can't be reached or picked. The three date pickers pass this intent straight through to the same Calendar.
Let the use case choose inline vs popover, and the nav mode fit the range. Show the grid inline when browsing the month is the task (availability, a range); tuck it behind a labelled trigger in a Popover for a form field. Keep the dropdown captions for arbitrary date entry; reach for gridNavigation when jumping across years is common. For availability views, numberOfMonths renders several months and fixedWeeks keeps their height stable.
Keep the range and today legible, and stay keyboard-operable. In range mode communicate the start, end, and the span between them (the preview band + the footer summary do this); today's treatment intentionally wins over selection so it's never swallowed. Arrow keys move by day, Page by month, and every day announces its full date — don't strip the focus ring or hide the month/year controls.
Best practices
- Choose
modeto fit the task (single / multiple / range) and holdselectedin state withonSelect. - Constrain choices with
minDate/maxDate/isDateUnavailablerather than validating after the fact — they clamp navigation too. - For a range, control it so the hover/focus preview band paints, and bound it with
minNights/maxNights. - Pair with Popover + a labeled trigger to build a date-picker field (or reach for DatePicker, which wires this).
- Don't force a calendar when a typed date is faster for the user.
- Don't re-derive a
disabledmatcher the constraints already build (that's the duplication this substrate removed). - Don't leave a popover trigger unlabeled when using the calendar as a field — the placeholder isn't a name.
- Don't reimplement day rendering — theme via
classNames/components, and use the publisheddata-slots.
Content guidelines
- Label the field or trigger with what the date is for (“Start date”).
- Show the selected date near the trigger in a clear, unambiguous format.
- Keep indicator dots to a legible few per day; they're decorative, so don't encode meaning only in color.
Accessibility
- Role
grid- ARIA APG
- grid
- Focus
- Roving focus across day cells; the focused day shows a ring. Initial focus lands on the selected day, or today clamped into the selectable window.
Keyboard
| ArrowLeftArrowRight | Move to the previous / next day (roving focus, react-day-picker). |
| ArrowUpArrowDown | Move to the same weekday in the previous / next week. |
| HomeEnd | First / last day of the week. |
| PageUpPageDown | Previous / next month. |
| EnterSpace | Select the focused day. In range mode, arrow-key focus also previews the band to the focused day (keyboard parity with hover). |
- • react-day-picker renders a labeled `role=grid` with roving focus; each day button exposes an `aria-label` (its full date), `aria-selected`, and disabled state.
- • Set `mode` (single | multiple | range) to match the selection you need; the selected shape follows (Date | Date[] | DateRange).
- • The in-flow prev/next nav buttons carry accessible names; the default dropdown captions are native `<select>`s (keyboard- and OS-native), each labelled.
- • Under `gridNavigation`, the caption becomes a labelled button (aria-label describes the period it changes) that drills into a year grid then a month grid.
- • The opt-in `showToday` footer is rendered inside react-day-picker's `role=status` / `aria-live=polite` region: a Today jump-and-select (auto-disabled when today falls outside min/max) plus a live selection summary.
- • Indicator dots are purely decorative — the container is `aria-hidden` and `pointer-events-none`, so they never affect the day's name or clickability (a per-day SR summary is a documented follow-up).
- • When used as the surface of a date field (inside a Popover), also give the trigger an accessible label — the placeholder is not a name.
- Status
- Stable
- Version
v0.3.0- Introduced
v0.1.0
garn doesn't yet record a per-release version history. This reflects the component's current lifecycle state; full changelog entries will appear once the metadata pipeline emits a lifecycle.history field.