General Resources, guides, and helpful links.
Blocks Larger composed sections for building pages.
Components Copy and paste UI components into your apps.
Animations Beautiful animations and effects for your UI.
A date field component that allows users to enter and edit date.
Blocks
We have built a collection of 30+ calendar blocks that you can use to build your own calendar components.
See all calendar blocks in the Blocks Library page.
Installation
Usage Copy import { Calendar } from "@/components/ui/calendar" Copy const [ date , setDate ] = React. useState < Date | undefined >( new Date ())
return (
< Calendar
mode = "single"
selected = { date }
onSelect = { setDate }
className = "rounded-lg border"
/>
)
About
Customization See the React DayPicker documentation for more information on how to customize the Calendar component.
Date Picker You can use the <Calendar> component to build a date picker. See the Date Picker page for more information.
Persian / Hijri / Jalali Calendar To use the Persian calendar, edit components/ui/calendar.tsx and replace react-day-picker with react-day-picker/persian.
Copy - import { DayPicker } from "react-day-picker"
+ import { DayPicker } from "react-day-picker/persian" خرداد ۱۴۰۴
ش ۱ش ۲ش ۳ش ۴ش ۵ش ج ۲۷ ۲۸ ۲۹ ۳۰ ۳۱ ۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ ۱۴ ۱۵ ۱۶ ۱۷ ۱۸ ۱۹ ۲۰ ۲۱ ۲۲ ۲۳ ۲۴ ۲۵ ۲۶ ۲۷ ۲۸ ۲۹ ۳۰ ۳۱ ۱ ۲ ۳ ۴ ۵ ۶
Selected Date (With TimeZone) The Calendar component accepts a timeZone prop to ensure dates are displayed and selected in the user's local timezone.
Copy export function CalendarWithTimezone () {
const [ date , setDate ] = React. useState < Date | undefined >( undefined )
const [ timeZone , setTimeZone ] = React. useState < string | undefined >( undefined )
React. useEffect (() => {
setTimeZone (Intl. DateTimeFormat (). resolvedOptions ().timeZone)
}, [])
return (
< Calendar
mode = "single"
selected = { date }
onSelect = { setDate }
timeZone = { timeZone }
/>
)
} Note: If you notice a selected date offset (for example, selecting the 20th highlights the 19th), make sure the timeZone prop is set to the user's local timezone.
Why client-side? The timezone is detected using Intl.DateTimeFormat().resolvedOptions().timeZone inside a useEffect to ensure compatibility with server-side rendering. Detecting the timezone during render would cause hydration mismatches, as the server and client may be in different timezones.
Examples
Range Calendar
Month and Year Selector
Date of Birth Picker
Date and Time Picker
Natural Language Picker This component uses the chrono-node library to parse natural language dates.
Custom Cell Size You can customize the size of calendar cells using the --cell-size CSS variable. You can also make it responsive by using breakpoint-specific values:
Copy < Calendar
mode = "single"
selected = { date }
onSelect = { setDate }
className = "rounded-lg border [--cell-size:--spacing(11)] md:[--cell-size:--spacing(12)]"
/> Copy < Calendar
mode = "single"
selected = { date }
onSelect = { setDate }
className = "rounded-lg border [--cell-size:2.75rem] md:[--cell-size:3rem]"
/>
Upgrade Guide
Tailwind v4 If you're already using Tailwind v4, you can upgrade to the latest version of the Calendar component by running the following command:
When you're prompted to overwrite the existing Calendar component, select Yes. If you have made any changes to the Calendar component, you will need to merge your changes with the new version.
This will update the Calendar component and react-day-picker to the latest version.
Next, follow the React DayPicker upgrade guide to upgrade your existing components to the latest version.
Installing Blocks After upgrading the Calendar component, you can install the new blocks by running the pitsi@latest add command.
This will install the latest version of the calendar blocks.
Tailwind v3 If you're using Tailwind v3, you can upgrade to the latest version of the Calendar by copying the following code to your calendar.tsx file.
If you have made any changes to the Calendar component, you will need to merge your changes with the new version.
Then follow the React DayPicker upgrade guide to upgrade your dependencies and existing components to the latest version.
Installing Blocks After upgrading the Calendar component, you can install the new blocks by running the pitsi@latest add command.
This will install the latest version of the calendar blocks.
Changelog
2025-10-26 Fixed day radius with week numbers We have fixed an issue where the selected day's left border radius was not applied correctly when week numbers were displayed. The fix ensures that when showWeekNumber is enabled, the first day (which is the second child due to the week number column) correctly receives the rounded left border.
To apply this fix, edit components/ui/calendar.tsx and update the day class in classNames:
components/ui/calendar.tsxCopy classNames = {{
// ... other classNames
day : cn (
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none" ,
props.showWeekNumber
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
: "[&:first-child[data-selected=true]_button]:rounded-l-md" ,
defaultClassNames.day
),
// ... other classNames
}} Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec Nov 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2025 November 2025 Su Mo Tu We Th Fr Sa 26 27 28 29 30 31 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 1 2 3 4 5 6
,
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:2rem] [[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 (
"relative flex flex-col gap-4 md:flex-row" ,
defaultClassNames.months
),
month: cn ( "flex w-full flex-col gap-4" , defaultClassNames.month),
nav: cn (
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1" ,
defaultClassNames.nav
),
button_previous: cn (
buttonVariants ({ variant: buttonVariant }),
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50" ,
defaultClassNames.button_previous
),
button_next: cn (
buttonVariants ({ variant: buttonVariant }),
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50" ,
defaultClassNames.button_next
),
month_caption: cn (
"flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]" ,
defaultClassNames.month_caption
),
dropdowns: cn (
"flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium" ,
defaultClassNames.dropdowns
),
dropdown_root: cn (
"has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border" ,
defaultClassNames.dropdown_root
),
dropdown: cn ( "absolute inset-0 opacity-0" , defaultClassNames.dropdown),
caption_label: cn (
"select-none font-medium" ,
captionLayout === "label"
? "text-sm"
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5" ,
defaultClassNames.caption_label
),
table: "w-full border-collapse" ,
weekdays: cn ( "flex" , defaultClassNames.weekdays),
weekday: cn (
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal" ,
defaultClassNames.weekday
),
week: cn ( "mt-2 flex w-full" , defaultClassNames.week),
week_number_header: cn (
"w-[--cell-size] select-none" ,
defaultClassNames.week_number_header
),
week_number: cn (
"text-muted-foreground select-none text-[0.8rem]" ,
defaultClassNames.week_number
),
day: cn (
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none" ,
props.showWeekNumber
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
: "[&:first-child[data-selected=true]_button]:rounded-l-md" ,
defaultClassNames.day
),
range_start: cn (
"bg-accent rounded-l-md" ,
defaultClassNames.range_start
),
range_middle: cn ( "rounded-none" , defaultClassNames.range_middle),
range_end: cn ( "bg-accent rounded-r-md" , 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 flex aspect-square h-auto w-full min-w-[--cell-size] flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] [&>span]:text-xs [&>span]:opacity-70" ,
defaultClassNames.day,
className
) }
{ ... props }
/>
)
}
export { Calendar, CalendarDayButton }
Copy "use client"
import * as React from "react"
import { Calendar } from "@/components/ui/calendar"
export function CalendarDemo () {
const [ date , setDate ] = React. useState < Date | undefined >( new Date ())
return (
< Calendar
mode = "single"
selected = {date}
onSelect = {setDate}
className = "rounded-md border shadow-sm"
captionLayout = "dropdown"
/>
)
}
Copy "use client"
import * as React from "react"
import { ChevronDown, ChevronLeft, ChevronRight } from "lucide-react"
import { DayButton, getDefaultClassNames } from "react-day-picker"
import { DayPicker } from "react-day-picker/persian"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
export function CalendarHijri () {
const [ date , setDate ] = React. useState < Date | undefined >(
new Date ( 2025 , 5 , 12 )
)
return (
< Calendar
mode = "single"
defaultMonth = {date}
selected = {date}
onSelect = {setDate}
className = "rounded-lg border shadow-sm"
/>
)
}
// ----------------------------------------------------------------------------
// The code below is for this example only.
// For your own calendar, you would edit the calendar.tsx component directly.
// ----------------------------------------------------------------------------
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 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 (
< ChevronLeft className = { cn ( "size-4" , className)} { ... props} />
)
}
if (orientation === "right" ) {
return (
< ChevronRight className = { cn ( "size-4" , className)} { ... props} />
)
}
return < ChevronDown 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}
/>
)
}