calendar
A date field component that allows users to enter and edit date.
Philosophy
Calendars are complex accessibility challenges disguised as simple grids. We own the entire calendar stack - from the headless engine (@gentleduck/calendar) to the compound primitives (@gentleduck/primitives/calendar) to this styled component. The adapter pattern lets you swap date libraries, the hook layer handles state management, and this component applies Tailwind styling via data-* attribute selectors.
How It's Built
Installation
npx @gentleduck/cli add calendar
npx @gentleduck/cli add calendar
Usage
import { Calendar } from "@/components/ui/calendar"import { Calendar } from "@/components/ui/calendar"const [date, setDate] = React.useState<Date | undefined>(new Date())
return (
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="rounded-lg border"
/>
)const [date, setDate] = React.useState<Date | undefined>(new Date())
return (
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="rounded-lg border"
/>
)See the @gentleduck/calendar package docs for more information on the headless engine, date adapters, and advanced features.
Examples
Date Picker (Popover)
Date Picker (Input)
Date Input (Natural Language)
Form Integration
Multi-Month Range Picker
Range Calendar
Date Constraints
Multi-Select
Event Calendar
Custom Cell Size
Click to View Events (Popover)
Booked Dates (Strikethrough)
Booking Calendar
Multi-Range Selection
Multi-Range with Shift+Click
Presets
Persian (Jalali) Calendar
Islamic (Hijri) Calendar
Hebrew Calendar
Hebrew Calendar (English)
Islamic Calendar (English)
Persian Calendar (English)
Notes
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.
Migrated from react-day-picker
We have replaced react-day-picker with our own headless calendar engine - @gentleduck/calendar. The result is 75% smaller bundle (~5 KB vs ~20 KB gzipped), zero external dependencies, full keyboard navigation, and complete ARIA compliance. See the performance benchmarks for detailed comparisons.
Date Picker
You can use the <Calendar> component to build a date picker. See the Date Picker page for more information.
RTL Support
Direction is resolved through the shared primitives direction module. Use a local dir="rtl" override when the component exposes it, or set DirectionProvider at app/root level for global RTL/LTR behavior.
Motion
Motion components work standalone, but some compositions may behave unexpectedly — this is still under active development. If you find a broken composition, please file an issue.
Use MotionCalendar for smooth month navigation transitions and staggered day cell entry powered by motion. The grid slides directionally when navigating months, and day cells fade in with a stagger effect.
Requires the motion package. Use MotionCalendar instead of Calendar. All props stay the same.
API Reference
Calendar
| Prop | Type | Default | Description |
|---|---|---|---|
adapter | Adapter.IDateAdapter<Date> | NativeAdapter | Date adapter for alternative calendar systems (Islamic, Persian, Hebrew) |
mode | 'single' | 'range' | 'multi' | 'multi-range' | 'single' | Selection mode |
selected | Date | DateRange<Date> | Date[] | null | - | Controlled selection value |
onSelect | (value) => void | - | Called when the selection changes |
disabled | Date[] | ((date: Date) => boolean) | - | Dates that cannot be selected |
defaultMonth | Date | - | Default month to display (uncontrolled) |
month | Date | - | Controlled displayed month |
onMonthChange | (month: Date) => void | - | Called when the displayed month changes |
showOutsideDays | boolean | true | Show days from adjacent months |
fixedWeeks | boolean | false | Always show 6 weeks |
numberOfMonths | number | 1 | How many months to show side by side |
showDropdowns | boolean | true | Show month/year dropdown selectors |
yearRange | { from: number; to: number } | { from: now-100, to: now+10 } | Range of years in the year dropdown |
locale | string | - | BCP 47 locale tag (e.g. 'ar-SA', 'ja-JP') |
dir | 'ltr' | 'rtl' | - | Text direction override |
fromDate | Date | - | Earliest selectable date |
toDate | Date | - | Latest selectable date |
buttonVariant | string | 'ghost' | Variant style for navigation buttons |
onDismiss | () => void | - | Called when the user presses Escape |
className | string | - | Additional CSS classes for the root container |
renderDay | (day: Grid.ICalendarDay<Date>, children: ReactNode) => ReactNode | - | Custom render function for day cell content |
renderHeader | (context: CalendarHeaderContext) => ReactNode | - | Custom render function for the navigation header |
renderWeekday | (day: string, index: number) => ReactNode | - | Custom render function for weekday column labels |
renderFooter | (months: Grid.ICalendarMonth<Date>[]) => ReactNode | - | Render content below the calendar grid |
Render Props
The Calendar component exposes render props that let you customize every visual part without forking the component.
renderDay
Customize the content inside each day cell. Receives the Grid.ICalendarDay object and the default children (the date number).
<Calendar
renderDay={(day, children) => (
<>
{children}
{hasEvents(day.date) && (
<span className="size-1 rounded-full bg-primary" />
)}
</>
)}
/><Calendar
renderDay={(day, children) => (
<>
{children}
{hasEvents(day.date) && (
<span className="size-1 rounded-full bg-primary" />
)}
</>
)}
/>The day object exposes: date, isToday, isSelected, isDisabled, isOutside, isHidden, isWeekend, isRangeStart, isRangeEnd, isRangeMiddle.
renderHeader
Replace the entire navigation header. Receives a context object with navigation controls.
<Calendar
renderHeader={({ month, title, direction, goToPrevMonth, goToNextMonth, isPrevDisabled, isNextDisabled }) => (
<div className="flex items-center justify-between px-2">
<button onClick={goToPrevMonth} disabled={isPrevDisabled}><-</button>
<span className="font-semibold">{title}</span>
<button onClick={goToNextMonth} disabled={isNextDisabled}>-></button>
</div>
)}
/><Calendar
renderHeader={({ month, title, direction, goToPrevMonth, goToNextMonth, isPrevDisabled, isNextDisabled }) => (
<div className="flex items-center justify-between px-2">
<button onClick={goToPrevMonth} disabled={isPrevDisabled}><-</button>
<span className="font-semibold">{title}</span>
<button onClick={goToNextMonth} disabled={isNextDisabled}>-></button>
</div>
)}
/>renderWeekday
Customize individual weekday column headers. Receives the abbreviation and column index.
<Calendar
renderWeekday={(day, index) => (
<span className={index === 0 || index === 6 ? "text-red-400" : ""}>
{day}
</span>
)}
/><Calendar
renderWeekday={(day, index) => (
<span className={index === 0 || index === 6 ? "text-red-400" : ""}>
{day}
</span>
)}
/>renderFooter
Add content below the calendar grid. Receives the current months array for context.
<Calendar
renderFooter={() => (
<div className="mt-2 text-xs text-muted-foreground">
Pick a date to continue
</div>
)}
/><Calendar
renderFooter={() => (
<div className="mt-2 text-xs text-muted-foreground">
Pick a date to continue
</div>
)}
/>Data Attributes
The calendar uses data-* attributes on day cells for styling. Target these in your CSS:
| Attribute | When Present |
|---|---|
data-selected="true" | Day is selected |
data-selected-single="true" | Day is selected (single mode, not part of range) |
data-today="true" | Day is today |
data-focused="true" | Day has keyboard focus |
data-range-start="true" | Day is the start of a range |
data-range-end="true" | Day is the end of a range |
data-range-middle="true" | Day is between range start and end |
data-day | Formatted date string for the day (always present) |
MotionCalendar
Adds directional slide transitions on month navigation (with blur) and staggered day cell entry animation. Uses LazyMotion for a lightweight bundle (~5KB). Requires the motion package.
| Prop | Type | Default | Description |
|---|---|---|---|
...props | CalendarProps | - | All props from Calendar are supported |
CSS Variables
| Variable | Default | Description |
|---|---|---|
--gentleduck-calendar-cell | --spacing(8) (32px) | Size of day cells and nav buttons |