Calendar
React calendar component with date selection, range picking, and keyboard navigation. Perfect for date pickers, booking systems, and form inputs.
Need users to pick dates? Whether it's booking appointments, selecting birthdays, or choosing delivery dates, users expect intuitive calendar interfaces. The Calendar component handles everything from single dates to complex range selections.
"use client"import * as React from "react"import { Calendar } from "@/components/ui/calendar"export default function CalendarDemo() { const [date, setDate] = React.useState<Date | undefined>(new Date()) return ( <div className="w-full p-6 flex justify-center"> <Calendar mode="single" selected={date} onSelect={setDate} className="rounded-md border shadow-sm" captionLayout="dropdown" /> </div> )}
"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.toLocaleDateString()} 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 }
Built on React DayPicker with full accessibility support, keyboard navigation, and customizable layouts that work on every device.
npx shadcn@latest add calendar
Why calendars work so well
Users understand calendar patterns from everywhere - phones, apps, booking sites:
- Visual date selection - See months at a glance instead of typing dates
- Keyboard navigation - Arrow keys and shortcuts work exactly as expected
- Range selection - Perfect for booking systems and date periods
- Accessibility first - Screen readers announce dates and navigation clearly
- Mobile friendly - Touch gestures work naturally on any device
- Form integration - Validates dates and handles edge cases automatically
- Customizable layouts - Single month, multiple months, or dropdown navigation
Common calendar patterns you'll build
Simple date selection
Basic calendar for picking single dates:
"use client"import * as React from "react"import { Calendar } from "@/components/ui/calendar"export default function CalendarSimple() { const [date, setDate] = React.useState<Date | undefined>(new Date()) return ( <div className="w-full p-6 flex justify-center"> <Calendar mode="single" selected={date} onSelect={setDate} className="rounded-lg border" /> </div> )}
"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.toLocaleDateString()} 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 }
Date range selection
Perfect for booking systems and period selection:
"use client"import * as React from "react"import { Calendar } from "@/components/ui/calendar"export default function CalendarRange() { const [date, setDate] = React.useState<Date | undefined>(new Date(2025, 5, 12)) return ( <div className="w-full p-6 flex justify-center"> <Calendar mode="single" defaultMonth={date} numberOfMonths={2} selected={date} onSelect={setDate} className="rounded-lg border shadow-sm" /> </div> )}
"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.toLocaleDateString()} 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 }
Multiple date selection
When users need to pick several individual dates:
"use client"import * as React from "react"import { DateRange } from "react-day-picker"import { Calendar } from "@/components/ui/calendar"export default function CalendarRangeSelection() { const [date, setDate] = React.useState<DateRange | undefined>({ from: new Date(2025, 0, 20), to: new Date(2025, 0, 27), }) return ( <div className="w-full p-6 flex justify-center"> <Calendar mode="range" defaultMonth={date?.from} selected={date} onSelect={setDate} numberOfMonths={2} className="rounded-lg border shadow-sm" /> </div> )}
"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.toLocaleDateString()} 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 }
Dropdown navigation
Quick month and year jumping for better UX:
"use client"import * as React from "react"import { Calendar } from "@/components/ui/calendar"import { Label } from "@/components/ui/label"import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/components/ui/select"export default function CalendarMonthYearSelector() { const [dropdown, setDropdown] = React.useState<React.ComponentProps<typeof Calendar>["captionLayout"]>( "dropdown" ) const [date, setDate] = React.useState<Date | undefined>( new Date(2025, 5, 12) ) return ( <div className="w-full p-6 flex justify-center"> <div className="flex flex-col gap-4"> <Calendar mode="single" defaultMonth={date} selected={date} onSelect={setDate} captionLayout={dropdown} className="rounded-lg border shadow-sm" /> <div className="flex flex-col gap-3"> <Label htmlFor="dropdown" className="px-1"> Dropdown </Label> <Select value={dropdown} onValueChange={(value) => setDropdown( value as React.ComponentProps<typeof Calendar>["captionLayout"] ) } > <SelectTrigger id="dropdown" size="sm" className="bg-background w-full" > <SelectValue placeholder="Dropdown" /> </SelectTrigger> <SelectContent align="center"> <SelectItem value="dropdown">Month and Year</SelectItem> <SelectItem value="dropdown-months">Month Only</SelectItem> <SelectItem value="dropdown-years">Year Only</SelectItem> </SelectContent> </Select> </div> </div> </div> )}
"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.toLocaleDateString()} 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 }
Popover date picker
Space-efficient date selection for forms:
"use client"import * as React from "react"import { ChevronDownIcon } from "lucide-react"import { Button } from "@/components/ui/button"import { Calendar } from "@/components/ui/calendar"import { Label } from "@/components/ui/label"import { Popover, PopoverContent, PopoverTrigger,} from "@/components/ui/popover"export default function CalendarDatePicker() { const [open, setOpen] = React.useState(false) const [date, setDate] = React.useState<Date | undefined>(undefined) return ( <div className="w-full p-6 flex justify-center"> <div className="flex flex-col gap-3"> <Label htmlFor="date" className="px-1"> Date of birth </Label> <Popover open={open} onOpenChange={setOpen}> <PopoverTrigger asChild> <Button variant="outline" id="date" className="w-48 justify-between font-normal" > {date ? date.toLocaleDateString() : "Select date"} <ChevronDownIcon /> </Button> </PopoverTrigger> <PopoverContent className="w-auto overflow-hidden p-0" align="start"> <Calendar mode="single" selected={date} captionLayout="dropdown" onSelect={(date) => { setDate(date) setOpen(false) }} /> </PopoverContent> </Popover> </div> </div> )}
import * as React from "react"import { Slot } from "@radix-ui/react-slot"import { cva, type VariantProps } from "class-variance-authority"import { cn } from "@/lib/utils"const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", icon: "size-9", }, }, defaultVariants: { variant: "default", size: "default", }, })function Button({ className, variant, size, asChild = false, ...props}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean }) { const Comp = asChild ? Slot : "button" return ( <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} /> )}export { Button, buttonVariants }
Date and time combination
Complete scheduling with time selection:
"use client"import * as React from "react"import { ChevronDownIcon } from "lucide-react"import { Button } from "@/components/ui/button"import { Calendar } from "@/components/ui/calendar"import { Input } from "@/components/ui/input"import { Label } from "@/components/ui/label"import { Popover, PopoverContent, PopoverTrigger,} from "@/components/ui/popover"export default function CalendarDateTimePicker() { const [open, setOpen] = React.useState(false) const [date, setDate] = React.useState<Date | undefined>(undefined) return ( <div className="w-full p-6 flex justify-center"> <div className="flex gap-4"> <div className="flex flex-col gap-3"> <Label htmlFor="date-picker" className="px-1"> Date </Label> <Popover open={open} onOpenChange={setOpen}> <PopoverTrigger asChild> <Button variant="outline" id="date-picker" className="w-32 justify-between font-normal" > {date ? date.toLocaleDateString() : "Select date"} <ChevronDownIcon /> </Button> </PopoverTrigger> <PopoverContent className="w-auto overflow-hidden p-0" align="start"> <Calendar mode="single" selected={date} captionLayout="dropdown" onSelect={(date) => { setDate(date) setOpen(false) }} /> </PopoverContent> </Popover> </div> <div className="flex flex-col gap-3"> <Label htmlFor="time-picker" className="px-1"> Time </Label> <Input type="time" id="time-picker" step="1" defaultValue="10:30:00" className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none" /> </div> </div> </div> )}
import * as React from "react"import { Slot } from "@radix-ui/react-slot"import { cva, type VariantProps } from "class-variance-authority"import { cn } from "@/lib/utils"const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", icon: "size-9", }, }, defaultVariants: { variant: "default", size: "default", }, })function Button({ className, variant, size, asChild = false, ...props}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean }) { const Comp = asChild ? Slot : "button" return ( <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} /> )}export { Button, buttonVariants }
Form validation integration
Proper error handling and validation:
"use client"import { zodResolver } from "@hookform/resolvers/zod"import { format } from "date-fns"import { CalendarIcon } from "lucide-react"import { useForm } from "react-hook-form"import { toast } from "sonner"import { z } from "zod"import { cn } from "@/lib/utils"import { Button } from "@/components/ui/button"import { Calendar } from "@/components/ui/calendar"import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage,} from "@/components/ui/form"import { Popover, PopoverContent, PopoverTrigger,} from "@/components/ui/popover"const FormSchema = z.object({ dob: z.date({ required_error: "A date of birth is required.", }),})export default function CalendarForm() { const form = useForm<z.infer<typeof FormSchema>>({ resolver: zodResolver(FormSchema), }) function onSubmit(data: z.infer<typeof FormSchema>) { toast("You submitted the following values", { description: ( <pre className="mt-2 w-[320px] rounded-md bg-neutral-950 p-4"> <code className="text-white">{JSON.stringify(data, null, 2)}</code> </pre> ), }) } return ( <div className="w-full p-6 flex justify-center"> <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <FormField control={form.control} name="dob" render={({ field }) => ( <FormItem className="flex flex-col"> <FormLabel>Date of birth</FormLabel> <Popover> <PopoverTrigger asChild> <FormControl> <Button variant={"outline"} className={cn( "w-[240px] pl-3 text-left font-normal", !field.value && "text-muted-foreground" )} > {field.value ? ( format(field.value, "PPP") ) : ( <span>Pick a date</span> )} <CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> </Button> </FormControl> </PopoverTrigger> <PopoverContent className="w-auto p-0" align="start"> <Calendar mode="single" selected={field.value} onSelect={field.onChange} disabled={(date) => date > new Date() || date < new Date("1900-01-01") } captionLayout="dropdown" /> </PopoverContent> </Popover> <FormDescription> Your date of birth is used to calculate your age. </FormDescription> <FormMessage /> </FormItem> )} /> <Button type="submit">Submit</Button> </form> </Form> </div> )}
import * as React from "react"import { Slot } from "@radix-ui/react-slot"import { cva, type VariantProps } from "class-variance-authority"import { cn } from "@/lib/utils"const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", icon: "size-9", }, }, defaultVariants: { variant: "default", size: "default", }, })function Button({ className, variant, size, asChild = false, ...props}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean }) { const Comp = asChild ? Slot : "button" return ( <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} /> )}export { Button, buttonVariants }
API Reference
Calendar
The main calendar component built on React DayPicker with full customization and accessibility support.
Prop | Type | Default | Description |
---|---|---|---|
mode | "single" | "multiple" | "range" | "single" | Date selection mode - single date, multiple dates, or date range |
selected | Date | Date[] | DateRange | undefined | - | Currently selected date(s) based on mode |
onSelect | (date?: Date | Date[] | DateRange) => void | - | Callback fired when date selection changes |
defaultMonth | Date | new Date() | Initial month to display |
month | Date | - | Controlled month display (use with onMonthChange) |
onMonthChange | (month: Date) => void | - | Callback when month navigation occurs |
numberOfMonths | number | 1 | Number of months to display side by side |
captionLayout | "label" | "dropdown" | "dropdown-months" | "dropdown-years" | "label" | Style of month/year navigation |
showOutsideDays | boolean | true | Show grayed-out dates from adjacent months |
fixedWeeks | boolean | false | Always show 6 weeks (42 days) for consistent height |
weekStartsOn | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 0 | First day of week (0 = Sunday, 1 = Monday, etc) |
disabled | Matcher | Matcher[] | - | Dates to disable (functions, arrays, or date objects) |
hidden | Matcher | Matcher[] | - | Dates to completely hide from view |
required | boolean | false | Whether date selection is required |
locale | Locale | - | date-fns locale for internationalization |
className | string | - | Additional CSS classes for the calendar container |
classNames | ClassNames | - | Custom styling for individual calendar elements |
components | CustomComponents | - | Override default components (buttons, navigation, etc) |
formatters | Formatters | - | Custom date formatting functions |
Selection Modes
Single Mode
Select one date at a time:
const [date, setDate] = useState<Date | undefined>()
<Calendar
mode="single"
selected={date}
onSelect={setDate}
/>
Multiple Mode
Select multiple individual dates:
const [dates, setDates] = useState<Date[]>([])
<Calendar
mode="multiple"
selected={dates}
onSelect={setDates}
/>
Range Mode
Select a date range with start and end:
import { DateRange } from "react-day-picker"
const [range, setRange] = useState<DateRange | undefined>()
<Calendar
mode="range"
selected={range}
onSelect={setRange}
numberOfMonths={2}
/>
Date Matchers
Powerful date matching for disabled/hidden dates:
Matcher Type | Example | Description |
---|---|---|
Date | new Date(2024, 11, 25) | Specific date |
Date[] | [date1, date2, date3] | Array of specific dates |
DateRange | { from: date1, to: date2 } | Date range |
DayOfWeek | { dayOfWeek: [0, 6] } | Weekends (Sunday=0, Saturday=6) |
Function | (date) => date < new Date() | Custom logic function |
DateBefore | { before: new Date() } | All dates before specified date |
DateAfter | { after: new Date() } | All dates after specified date |
Caption Layouts
Layout | Description |
---|---|
"label" | Simple text with arrow navigation (default) |
"dropdown" | Both month and year as dropdown selectors |
"dropdown-months" | Only month selector, year as text |
"dropdown-years" | Only year selector, month as text |
Keyboard Navigation
Key | Action |
---|---|
Arrow Keys | Navigate between dates |
Enter / Space | Select focused date |
Home | Go to start of week |
End | Go to end of week |
Page Up | Previous month |
Page Down | Next month |
Shift + Page Up | Previous year |
Shift + Page Down | Next year |
Tab | Move to navigation controls |
Calendar best practices
What makes calendars feel intuitive and accessible:
- Show current date clearly - Today should always be visually distinct
- Logical navigation - Month/year dropdowns for quick jumping, arrows for step-by-step
- Disable invalid dates - Gray out past dates, weekends, or blocked periods
- Range feedback - Show start, middle, and end states clearly for range selection
- Keyboard support - Arrow keys, Enter, and shortcuts should work perfectly
- Mobile friendly - Touch targets are large enough for easy tapping
- Form integration - Validate dates and show clear error messages
- Loading states - Show skeleton or placeholder while data loads