Calendar
A date-field calendar built on React DayPicker — single, multiple and range selection, month and year dropdowns, week numbers and custom cell sizes.
- Status
- Since
0.10.0- Accessibility pattern
- APG date grid via React DayPicker
Last updated
Install
Add Calendar from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/calendarThe same command installs the registry items it composes: @vegastack/button.
It also adds the sanctioned engine to your package.json: react-day-picker (calendar interaction/rendering engine).
Usage
import { Calendar } from "@/components/ui/calendar";
const [date, setDate] = React.useState<Date | undefined>(new Date());
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="rounded-lg border"
/>;Anatomy
Examples
About
Calendar is React DayPicker with our chrome on top: the day grid, the range logic and the month
navigation are the library's; the tokens, the CalendarDayButton and the nav buttons are ours.
Every prop DayPicker takes passes straight through.
The day grid, the range logic and the month navigation are React DayPicker; the chrome, the tokens and the day button are ours.
Date Picker
Put a Calendar inside a Popover behind a Button and you have a date picker; see
Date Picker for the composed component.
Put this inside a Popover behind a Button and you have a date picker.
Persian / Hijri / Jalali Calendar
The chrome mirrors for a right-to-left document on its own. To change the calendar system, edit
components/ui/calendar.tsx and swap the import:
- import { DayPicker } from "react-day-picker"
+ import { DayPicker } from "react-day-picker/persian"The chrome mirrors for a right-to-left document on its own. Swapping the calendar system is one import in components/ui/calendar.tsx: react-day-picker/persian.
Selected Date (With TimeZone)
Pass timeZone so a selected day is the day the user sees. Detect it in an effect, not during
render — the server and the client are not in the same zone, and reading it during render is a
hydration mismatch.
Time zone: detecting…
Basic
className="rounded-lg border" is all the surface a bare calendar needs.
Range Calendar
mode="range" selects a span; numberOfMonths shows more than one month at a time.
Month and Year Selector
captionLayout="dropdown" replaces the caption with month and year dropdowns.
Presets
Drive month and selected from outside to offer shortcuts.
Date and Time Picker
A Card with the calendar in its content and two time fields in its footer.
Booked dates
disabled blocks the days; modifiers and modifiersClassNames mark them, so unavailability is
carried by the strike-through as well as by the dimming.
Custom Cell Size
--cell-size sets the grid's cell, and it takes breakpoint-specific values. A custom DayButton
renders anything you like inside a day.
Week Numbers
showWeekNumber adds the ISO week column.
RTL
The two nav chevrons rotate and the range caps swap ends, so a right-to-left document reads correctly without any extra work.
States
Selected, today, disabled and outside days in one frame.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
buttonVariant | "default" | "destructive" | "ghost" | "link" | "outline" | "secondary" | — |
Everything else is React DayPicker's own API — see its
documentation. CalendarDayButton accepts DayPicker's DayButton props
plus an optional locale.
Accessibility
- React DayPicker renders the month as a real
<table>with column headers, and the day buttons carryaria-selected,aria-disabledanddata-day; ←/→, ↑/ ↓, Home/End and PageUp/ PageDown all move the focused day. - The day button is a
Button size="icon"at--cell-size(28px by default), so it clears the 24px target floor (A11Y-2) without a hit-area pseudo-element. - Focus is the global 2px
:focus-visibleoutline (FOC-1); upstream'sring-[3px]glow on the focused day is removed (FOC-6). - Booked or unavailable days should carry a non-colour marker as well as the dim — the Booked dates example uses a strike-through (A11Y-8).
Calendaris a client component: it holds the focused day and reacts to selection.
| Contract | States tested |
|---|---|
| Behaviour | default, selected, range, disabled, outside, today |
| Accessibility | native-or-base-ui-semantics, browser-accessibility-test, keyboard-navigation, selected, disabled |
| Visual | default, hover, selected, range, disabled, today |
Do / Don't
Deviations
Upstream's file plus packages/ui/upstream/patches/calendar.patch. Every hunk:
- FOC-1, FOC-6 — the focused day's
group-data-[focused=true]/day:border-ring,:ring-[3px]and:ring-ring/50glow is removed. The day button is a real<button>, so the global:focus-visibleoutline inbase.cssmarks it; therelative z-10that lifted the focused cell above its neighbours stays. - A11Y-2 —
dropdown_rootgainsh-full min-h-6. WithcaptionLayout="dropdown"the real<select>isabsolute inset-0of that root, which upstream sizes from the caption's 20px line box — a 20px-tall pointer target. The dropdowns row is alreadyh-(--cell-size), so filling it costs no layout. - DOC-2 —
cnis imported from@vegastack/design. - DOC-1 — prettier reflow plus the
registry-stamp.mjsprovenance header the three-copy model needs; no decision of its own.