Shadcn Calendar
React calendar component for date picking with ranges and keyboard navigation. Built with TypeScript and Tailwind CSS for Next.js using React DayPicker.
Calendar component buggy?
Join our Discord community for help from other developers.
Ever watched a user type "31/02/2024" into a date field and wonder why your app crashed? Or had someone enter their birthday as "yesterday"? Text inputs for dates are where good UX goes to die. This shadcn/ui calendar gives users a visual way to pick dates that actually exist.
Calendar showcase
Complete date picker with dropdown navigation:
"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 TypeScript types and keyboard navigation. Styled with Tailwind CSS so it matches your design system, not some random jQuery plugin from 2012.
npx shadcn@latest add calendar
Why calendars actually prevent errors
Here's the thing—asking users to type dates is asking for trouble. Different countries use different formats (MM/DD/YYYY vs DD/MM/YYYY vs YYYY-MM-DD). Users make typos. They enter impossible dates like February 30th. They forget leap years exist.
A visual calendar solves all of this. Users see the days, click what they want, done. No format confusion, no invalid dates, no parsing nightmares in your backend. It's not just better UX—it's fewer bugs in production.
This free shadcn calendar handles the complex parts—date math, keyboard navigation, accessibility—while you focus on your business logic. Whether you're building booking systems, form inputs, or scheduling features in your JavaScript apps, calendars that actually work save everyone's sanity.
Common calendar patterns you'll actually use
Date range selection
Perfect for booking systems and reports:
"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 }
Date picker in popover
Space-efficient 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 }
Form integration
With validation and error handling:
"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 }
Month and year navigation
Quick jumping for birth dates and historical data:
"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 }
Features
This free open source calendar component includes everything you need:
- TypeScript-first - Full type safety with Date objects and selections
- React DayPicker powered - Battle-tested calendar logic and accessibility
- Tailwind CSS styled - Customize with utilities, not fighting CSS
- Keyboard navigation - Arrow keys, page up/down, everything works
- Range selection - Pick start and end dates for periods
- Multiple modes - Single date, multiple dates, or date ranges
- Accessible by default - Screen readers announce everything properly
- Mobile optimized - Touch-friendly with proper tap targets
API Reference
Calendar Props
Prop | Type | Default | Description |
---|---|---|---|
mode | "single" | "multiple" | "range" | "single" | Selection mode |
selected | Date | Date[] | DateRange | - | Selected date(s) |
onSelect | Function | - | Selection handler |
disabled | Matcher | Matcher[] | - | Dates to disable |
captionLayout | "label" | "dropdown" | "label" | Month/year navigation style |
Date Matchers
Flexible ways to disable dates:
Type | Example | Use Case |
---|---|---|
Date | new Date(2024, 11, 25) | Christmas |
DateRange | { from: date1, to: date2 } | Vacation period |
DayOfWeek | { dayOfWeek: [0, 6] } | Weekends |
Function | (date) => date < new Date() | Past dates |
Keyboard Shortcuts
Key | Action |
---|---|
Arrow Keys | Navigate dates |
Enter/Space | Select date |
Page Up/Down | Previous/next month |
Shift + Page Up/Down | Previous/next year |
Production tips
Always show today clearly. This free shadcn/ui calendar highlights today by default, but make it obvious. Users orient themselves from "now" and work forwards or backwards. Your React calendar should make today impossible to miss.
Disable what can't be selected. Booking system? Disable past dates. Age verification? Disable future dates. Weekend delivery? Disable Saturdays and Sundays. This TypeScript component makes it easy with matcher functions.
Mobile needs bigger tap targets. Those tiny date numbers look fine on desktop. On mobile? Good luck hitting the right date with a thumb. The calendar adapts, but test on real devices. Your Next.js app should work for fingers, not just mouse cursors.
Consider timezone chaos. User picks "tomorrow" at 11pm their time, but your server thinks it's already tomorrow. Dates are hard. This open source shadcn component gives you Date objects—handle timezones in your logic.
Range selection needs visual feedback. When picking check-in/check-out dates, show the range as users hover. The selected period should be obvious. Your JavaScript calendar should guide users, not confuse them.
Integration with other components
Calendars rarely live alone. Wrap them in Popover components for space-efficient date pickers in your React applications. The trigger button shows the selected date, clicking reveals the calendar.
For forms, combine calendars with Form validation to handle date requirements. This open source pattern ensures users pick valid dates before submission. Add Input components for manual date entry as a fallback—some users prefer typing.
In booking interfaces, pair calendars with Select components for time slots. Your JavaScript components work together—pick a date, then choose available times. The calendar provides the day, other components handle the details.
Questions you might have
Shadcn Button
React button component with variants and loading states. Built with TypeScript and Tailwind CSS for Next.js applications using class-variance-authority.
Shadcn Card
React card component for organizing content with headers, actions, and footers. Built with TypeScript and Tailwind CSS for Next.js with semantic HTML.