Shadcn Calendar
Free shadcn/ui calendar for React — built on react-day-picker v9. Single, range, and multiple selection, month/year dropdowns, disabled dates, two-up months, presets, and an appointment-booking flow. The examples official shadcn never shipped.
Installation
{
"registries": {
"@designrevision": "https://registry.designrevision.com/r/{name}.json"
}
}
Add to your existing components.json. Requires shadcn/ui v2.3+.
Packages
Props
mode
= "single"
"single" | "range" | "multiple"
Selection mode (forwarded to react-day-picker). Pair with selected / onSelect.
selected
= —
Date | Date[] | DateRange
The selected date(s); shape matches mode.
onSelect
= —
(value) => void
Fires when the selection changes.
captionLayout
= "label"
"label" | "dropdown" | "dropdown-months" | "dropdown-years"
Header style — a static label or month/year dropdowns.
numberOfMonths
= 1
number
How many months to render side by side.
disabled
= —
Matcher | Matcher[]
Dates to disable — { before } / { after } / { dayOfWeek } / specific dates.
The shadcn calendar built on react-day-picker v9 + date-fns. Exports Calendar and CalendarDayButton; forwards all DayPicker props (mode, selected, onSelect, month, captionLayout, numberOfMonths, disabled, modifiers, etc.). For a date input that opens this in a popover, compose it with Popover (the Date Picker pattern). Requires react-day-picker v9 — v8 classNames will not match.
"use client"
import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {
const defaultClassNames = getDefaultClassNames()
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"flex gap-4 flex-col md:flex-row relative",
defaultClassNames.months
),
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
nav: cn(
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_next
),
month_caption: cn(
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
defaultClassNames.month_caption
),
dropdowns: cn(
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
defaultClassNames.dropdown_root
),
dropdown: cn(
"absolute bg-popover inset-0 opacity-0",
defaultClassNames.dropdown
),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
defaultClassNames.caption_label
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
defaultClassNames.weekday
),
week: cn("flex w-full mt-2", defaultClassNames.week),
week_number_header: cn(
"select-none w-(--cell-size)",
defaultClassNames.week_number_header
),
week_number: cn(
"text-[0.8rem] select-none text-muted-foreground",
defaultClassNames.week_number
),
day: cn(
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
defaultClassNames.day
),
range_start: cn(
"rounded-l-md bg-accent",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
)
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
)
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("size-4", className)}
{...props}
/>
)
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
)
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center">
{children}
</div>
</td>
)
},
...components,
}}
{...props}
/>
)
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toISOString().split("T")[0]}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
)
}
export { Calendar, CalendarDayButton }
Examples
Calendar
A single-date calendar in a bordered card with a controlled `selected` date.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import { Calendar } from "@/components/ui/calendar"
export default function CalendarDemo01() {
const [date, setDate] = React.useState<Date | undefined>(new Date())
return (
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="rounded-md border shadow-sm"
/>
)
}
Range
Date-range selection with `mode="range"` — pick a start and end day.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import { addDays } from "date-fns"
import { type DateRange } from "react-day-picker"
import { Calendar } from "@/components/ui/calendar"
export default function CalendarRange01() {
const [range, setRange] = React.useState<DateRange | undefined>({
from: new Date(),
to: addDays(new Date(), 5),
})
return (
<Calendar
mode="range"
selected={range}
onSelect={setRange}
className="rounded-md border shadow-sm"
/>
)
}
Multiple dates
Select several individual dates with `mode="multiple"` and a `max`.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import { Calendar } from "@/components/ui/calendar"
export default function CalendarMultiple01() {
const [dates, setDates] = React.useState<Date[] | undefined>([])
return (
<Calendar
mode="multiple"
max={5}
selected={dates}
onSelect={setDates}
className="rounded-md border shadow-sm"
/>
)
}
Month & year dropdowns
`captionLayout="dropdown"` with `startMonth` / `endMonth` bounds — fast navigation for date-of-birth pickers.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import { Calendar } from "@/components/ui/calendar"
export default function CalendarDropdown01() {
const [date, setDate] = React.useState<Date | undefined>(new Date(1990, 0, 1))
return (
<Calendar
mode="single"
selected={date}
onSelect={setDate}
captionLayout="dropdown"
startMonth={new Date(1980, 0)}
endMonth={new Date(2030, 11)}
className="rounded-md border shadow-sm"
/>
)
}
Disabled dates
Block past dates and weekends with the `disabled` matcher — `{ before }` plus `{ dayOfWeek }`.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import { Calendar } from "@/components/ui/calendar"
export default function CalendarDisabled01() {
const [date, setDate] = React.useState<Date | undefined>()
return (
<Calendar
mode="single"
selected={date}
onSelect={setDate}
// No past dates and no weekends.
disabled={[{ before: new Date() }, { dayOfWeek: [0, 6] }]}
className="rounded-md border shadow-sm"
/>
)
}
Two months
A range across two months side by side with `numberOfMonths={2}` — the date-range-picker layout.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import { addDays } from "date-fns"
import { type DateRange } from "react-day-picker"
import { Calendar } from "@/components/ui/calendar"
export default function CalendarTwoMonths01() {
const [range, setRange] = React.useState<DateRange | undefined>({
from: new Date(),
to: addDays(new Date(), 8),
})
return (
<Calendar
mode="range"
selected={range}
onSelect={setRange}
numberOfMonths={2}
defaultMonth={range?.from}
className="rounded-md border shadow-sm"
/>
)
}
With presets
Quick-select buttons (Today / Tomorrow / In a week) beside the calendar, with a selected-date footer.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import { addDays, format } from "date-fns"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
const presets = [
{ label: "Today", days: 0 },
{ label: "Tomorrow", days: 1 },
{ label: "In a week", days: 7 },
{ label: "In a month", days: 30 },
]
export default function CalendarPresets01() {
const [date, setDate] = React.useState<Date | undefined>(new Date())
const [month, setMonth] = React.useState<Date>(new Date())
const selectPreset = (days: number) => {
const next = addDays(new Date(), days)
setDate(next)
setMonth(next)
}
return (
<div className="rounded-md border shadow-sm">
<div className="flex max-sm:flex-col">
<div className="flex flex-col gap-1 border-b p-2 sm:border-e sm:border-b-0">
{presets.map((preset) => (
<Button
key={preset.label}
variant="ghost"
size="sm"
className="justify-start font-normal"
onClick={() => selectPreset(preset.days)}
>
{preset.label}
</Button>
))}
</div>
<Calendar
mode="single"
selected={date}
onSelect={setDate}
month={month}
onMonthChange={setMonth}
/>
</div>
<div className="border-t p-3 text-center text-sm">
{date ? (
<>
Selected <span className="font-medium">{format(date, "PPP")}</span>
</>
) : (
"Pick a date"
)}
</div>
</div>
)
}
Appointment booking
A booking flow — disabled past/weekend/booked days with a struck-through `booked` modifier and a time-slot panel.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
const today = new Date()
// A few already-booked (unavailable) days over the next two weeks.
const bookedDates = [3, 5, 8, 11].map((offset) => {
const date = new Date(today)
date.setDate(today.getDate() + offset)
return date
})
const timeSlots = [
"9:00 AM",
"9:30 AM",
"10:00 AM",
"10:30 AM",
"11:00 AM",
"1:00 PM",
"1:30 PM",
"2:00 PM",
]
export default function CalendarBooking01() {
const [date, setDate] = React.useState<Date | undefined>()
const [time, setTime] = React.useState<string | null>(null)
return (
<div className="rounded-md border shadow-sm">
<div className="flex max-sm:flex-col">
<Calendar
mode="single"
selected={date}
onSelect={(value) => {
setDate(value)
setTime(null)
}}
disabled={[{ before: today }, { dayOfWeek: [0, 6] }, ...bookedDates]}
modifiers={{ booked: bookedDates }}
modifiersClassNames={{ booked: "[&>button]:line-through" }}
className="p-2 sm:border-e"
/>
<div className="w-full space-y-2 p-3 sm:w-44">
<p className="text-sm font-medium">
{date
? date.toLocaleDateString("en-US", {
weekday: "long",
month: "long",
day: "numeric",
})
: "Select a date"}
</p>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-1">
{timeSlots.map((slot) => (
<Button
key={slot}
variant={time === slot ? "default" : "outline"}
size="sm"
disabled={!date}
onClick={() => setTime(slot)}
>
{slot}
</Button>
))}
</div>
</div>
</div>
</div>
)
}
The Shadcn Calendar is the date grid — the canonical shadcn/ui calendar built on react-day-picker v9. Official ships one single-date demo; the examples below cover the real range — range and multiple selection, month/year dropdowns, disabled dates, two-up months, presets, and a booking flow.
Built on react-day-picker
The Calendar is a styled wrapper over react-day-picker (with date-fns for the date math) — the headless library that owns the calendar grid, keyboard navigation, and the selection modes. The shadcn layer adds Tailwind styling, your design tokens, and a CalendarDayButton built from the Button. Install the two dependencies:
npm install react-day-picker date-fns
Version matters: this Calendar targets react-day-picker v9. It uses getDefaultClassNames(), the v9 components override map, and v9 classNames keys — none of which exist in v8, so a v8 install renders the calendar unstyled. Pin react-day-picker to ^9.
Selection modes
The mode prop sets what you can pick:
single— one date.selectedis aDate.range— a start and end.selectedis aDateRange({ from, to }).multiple— a list.selectedis aDate[], with an optionalmax.
Each pairs with an onSelect of the matching shape. The Calendar, Range, and Multiple dates examples cover all three.
Month & year navigation
By default the header is a static label with prev/next arrows. Set captionLayout="dropdown" (and bound it with startMonth / endMonth) to turn the month and year into dropdowns — the right choice for date-of-birth fields where paging month by month is painful. The Month & year dropdowns example jumps decades in two clicks.
Disabling dates
Pass a matcher (or array) to disabled:
disabled={[{ before: new Date() }, { dayOfWeek: [0, 6] }]}
{ before } / { after } block ranges, { dayOfWeek: [0, 6] } blocks weekends, and an array of Dates blocks specific days. The Disabled dates example blocks the past and weekends; the Booking example also blocks individual booked days.
Layout, presets & ranges
Render two months side by side with numberOfMonths={2} for a date-range UI (the Two months example). Pair the calendar with preset buttons — Today, Tomorrow, In a week — by setting both the selected date and the visible month, and show the choice in a footer (the With presets example).
Booking & custom days
For scheduling, combine disabled (past, weekends, and already-booked days) with a booked modifier and modifiersClassNames to strike through unavailable days, then show time slots beside the grid. The Appointment booking example is the full flow. For deeper customization, the exported CalendarDayButton lets you render anything inside a day cell (prices, dots, badges).
Calendar vs Date Picker
This page is the standalone Calendar. A Date Picker is the same Calendar inside a Popover behind a button, so the field stays compact and the grid opens on click — compose Calendar + Popover to build one. (That's a separate pattern, like Table vs Data Table.)
Accessibility & theming
react-day-picker handles the hard parts — a labelled grid, full arrow-key navigation, and focus management (the CalendarDayButton focuses the active day). The calendar reads your design tokens — --accent (today and ranges), --primary (the selected day), --muted-foreground (weekday headers) — so it follows your theme, including dark mode, with no overrides.