rapidUI

Calendar

Date grid built on react-day-picker, used by the date pickers.

Installation

pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/calendar.json

Installs to @/components/ui/calendar.

Manual installation

Install the dependencies:

pnpm add lucide-react@^1.48.0 react-day-picker@^10.0.1

Add the components it builds on: button.

Copy the source into your project:

components/ui/calendar.tsx
'use client'

import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'
import * as React from 'react'
import { type DayButton, DayPicker, getDefaultClassNames, type Locale, useDayPicker } from 'react-day-picker'

import { Button, buttonVariants } from '@/components/ui/button'
import { cn } from '@/lib/utils'

function Calendar({
	className,
	classNames,
	showOutsideDays = true,
	captionLayout = 'label',
	buttonVariant = 'ghost',
	// Weeks start on Monday by default, as in the Nordic countries; pass weekStartsOn to override
	weekStartsOn = 1,
	locale,
	formatters,
	components,
	...props
}: React.ComponentProps<typeof DayPicker> & {
	buttonVariant?: React.ComponentProps<typeof Button>['variant']
}) {
	const defaultClassNames = getDefaultClassNames()

	return (
		<DayPicker
			showOutsideDays={showOutsideDays}
			weekStartsOn={weekStartsOn}
			className={cn(
				'group/calendar bg-background p-2 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(9)] in-data-[slot=card-content]:bg-transparent in-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}
			locale={locale}
			formatters={{
				formatMonthDropdown: (date) => date.toLocaleString(locale?.code, { 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 }),
					'size-(--cell-size) p-0 select-none aria-disabled:opacity-50',
					defaultClassNames.button_previous,
				),
				button_next: cn(
					buttonVariants({ variant: buttonVariant }),
					'size-(--cell-size) p-0 select-none 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(
					'relative rounded-(--cell-radius) has-focus:ring-3 has-focus:ring-ring',
					defaultClassNames.dropdown_root,
				),
				dropdown: cn('absolute inset-0 bg-popover opacity-0', defaultClassNames.dropdown),
				caption_label: cn(
					'font-medium select-none',
					captionLayout === 'label'
						? 'text-sm'
						: 'flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground',
					defaultClassNames.caption_label,
				),
				month_grid: cn('w-full border-collapse', defaultClassNames.month_grid),
				weekdays: cn('flex', defaultClassNames.weekdays),
				weekday: cn(
					'flex-1 rounded-(--cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none',
					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-[0.8rem] text-muted-foreground select-none', defaultClassNames.week_number),
				day: cn(
					'group/day relative aspect-square h-full w-full rounded-(--cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)',
					props.showWeekNumber
						? '[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)'
						: '[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)',
					defaultClassNames.day,
				),
				range_start: cn(
					'relative isolate z-0 rounded-l-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted',
					defaultClassNames.range_start,
				),
				range_middle: cn('rounded-none', defaultClassNames.range_middle),
				range_end: cn(
					'relative isolate z-0 rounded-r-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted',
					defaultClassNames.range_end,
				),
				today: cn(
					'rounded-(--cell-radius) bg-muted text-foreground 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: CalendarLocalizedDayButton,
				WeekNumber: ({ children, ...props }) => {
					return (
						<td {...props}>
							<div className="flex size-(--cell-size) items-center justify-center text-center">{children}</div>
						</td>
					)
				},
				...components,
			}}
			{...props}
		/>
	)
}

/** The day button `Calendar` hands to DayPicker: a `CalendarDayButton` in the locale the calendar was given. */
function CalendarLocalizedDayButton(props: Readonly<React.ComponentProps<typeof DayButton>>) {
	const { dayPickerProps } = useDayPicker()
	return <CalendarDayButton locale={dayPickerProps.locale} {...props} />
}

function CalendarDayButton({
	className,
	day,
	modifiers,
	locale,
	...props
}: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {
	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(locale?.code)}
			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(
				'relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-muted data-[range-middle=true]:text-foreground data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70',
				defaultClassNames.day,
				className,
			)}
			{...props}
		/>
	)
}

export { Calendar, CalendarDayButton }

A flexible and accessible calendar component built on top of react-day-picker v10. Supports single date selection, date ranges, multiple dates, and various display modes with full keyboard navigation and screen reader support.

Features

  • Single date, date range, and multiple date selection modes
  • Dropdown navigation for months and years
  • Caption layouts: label, dropdown, dropdown-months, dropdown-years
  • Week numbers display
  • Weeks start on Monday by default (weekStartsOn={1}); pass weekStartsOn={0} for Sunday
  • Disabled dates and date ranges
  • Multiple month display
  • Accessible keyboard navigation
  • Screen reader support
  • Custom styling with Tailwind CSS
  • Localization via the locale prop (e.g. sv from react-day-picker/locale)
  • Outside days shown by default (showOutsideDays)
  • Configurable navigation button variant (buttonVariant)

Usage

import { Calendar } from '@/components/ui/calendar'
import type { DateRange } from 'react-day-picker'

// Basic usage
export function MyComponent() {
  const [selected, setSelected] = React.useState<Date>()
  
  return (
    <Calendar
      mode="single"
      selected={selected}
      onSelect={setSelected}
    />
  )
}

// Date range selection
export function DateRangeComponent() {
  const [selected, setSelected] = React.useState<DateRange>()
  
  return (
    <Calendar
      mode="range"
      selected={selected}
      onSelect={setSelected}
    />
  )
}

// Multiple date selection
export function MultipleDatesComponent() {
  const [selected, setSelected] = React.useState<Date[]>([])
  
  return (
    <Calendar
      mode="multiple"
      selected={selected}
      onSelect={setSelected}
    />
  )
}

Examples

Default

A single date, opened on a fixed month with defaultMonth. Weeks start on Monday.

With Dropdown Navigation

Month and year dropdowns (captionLayout="dropdown") make dates far back, like a date of birth, quick to reach. startMonth and endMonth limit the years in the dropdown.

Date Range Selection

mode="range" inside a card, for pausing home help while the resident is away. The footer summarises the selection.

Multiple Date Selection

mode="multiple" for picking individual days, with weekends disabled.

With Preselected Date

With required, a date is always selected and onSelect always receives a Date.

With Disabled Dates

A booking card where past days, weekends and fully booked days are disabled by passing matchers and dates to disabled, next to a list of time slots.

Multiple Months Display

Two months side by side (numberOfMonths={2}) make ranges that cross a month boundary easier to select.

With Week Numbers

showWeekNumber adds ISO week numbers, which are widely used in the Nordic countries. Custom modifiers and modifiersClassNames mark waste collection days, with a legend that explains the marker.

Custom Week Start

Weeks start on Monday by default. Pass weekStartsOn to change it, e.g. weekStartsOn={0} for Sunday.

With Min/Max Dates

Calendar with restricted date selection and navigation range (disabled={{ before, after }} together with startMonth / endMonth).

Caption Layouts

Month-only and year-only dropdowns.

With Locale

Swedish and Finnish month and weekday names via locale={sv} and locale={fi}.

Outline Navigation

Navigation buttons rendered with buttonVariant="outline".

Fixed Weeks

Always render six weeks per month, so the height doesn’t change when navigating.

Props

Calendar

  • mode - Selection mode ('single' | 'multiple' | 'range')
  • selected - Currently selected date(s)
  • onSelect - Callback when selection changes
  • captionLayout - Layout of the month/year caption ('label' | 'dropdown' | 'dropdown-months' | 'dropdown-years'), defaults to 'label'
  • showOutsideDays - Whether to show days from adjacent months (defaults to true)
  • numberOfMonths - Number of months to display simultaneously
  • weekStartsOn - First day of the week (0=Sunday, 1=Monday, etc.); defaults to 1 (Monday)
  • showWeekNumber - Whether to display week numbers
  • fixedWeeks - Always display six weeks
  • disabled - Dates or matchers to disable (e.g. { before: date }, { after: date }, { dayOfWeek: [0, 6] })
  • startMonth / endMonth - Navigation boundaries (also limit the dropdown years)
  • locale - A date-fns / react-day-picker locale
  • timeZone - IANA time zone for calendar math
  • className - Additional CSS classes
  • buttonVariant - Button variant for the navigation buttons ('default' | 'outline' | 'secondary' | 'ghost' | 'destructive' | 'link'), defaults to 'ghost'

All other react-day-picker DayPicker props are supported. CalendarDayButton is also exported for custom components={{ DayButton }} overrides.

Date Selection Types

Single Mode

const [selected, setSelected] = React.useState<Date>()
<Calendar mode="single" selected={selected} onSelect={setSelected} />

Range Mode

const [selected, setSelected] = React.useState<DateRange>()
<Calendar mode="range" selected={selected} onSelect={setSelected} />

Multiple Mode

const [selected, setSelected] = React.useState<Date[]>([])
<Calendar mode="multiple" selected={selected} onSelect={setSelected} />

Accessibility

The Calendar component follows WAI-ARIA guidelines:

  • Uses proper ARIA roles (grid, gridcell, button)
  • Supports comprehensive keyboard navigation:
    • Arrow keys: Navigate between dates
    • Space/Enter: Select date
    • Page Up/Down: Navigate months
    • Shift + Page Up/Down: Navigate years
    • Home/End: Navigate to week start/end
    • Ctrl + Home: Navigate to today
  • Announces date changes to screen readers
  • Maintains focus management during navigation
  • Provides clear visual feedback for all states
  • Supports high contrast mode

Best Practices

  1. Date Selection:

    • Provide clear visual feedback for selected dates
    • Use appropriate selection mode for the use case
    • Consider date range limitations when needed
  2. Navigation:

    • Use dropdown navigation for large date ranges
    • Provide year boundaries to prevent excessive scrolling
    • Consider starting week day based on user locale
  3. Accessibility:

    • Ensure sufficient color contrast for all states
    • Provide keyboard navigation instructions
    • Test with screen readers
    • Maintain logical tab order
  4. User Experience:

    • Show multiple months for range selection
    • Disable irrelevant dates clearly
    • Provide feedback for selected dates
    • Consider mobile touch targets
  5. Localization:

    • Configure week start day based on locale
    • Use appropriate date formatting
    • Support right-to-left languages
    • Provide translated month/day names
  • Datetime Picker - For date and time selection
  • Time Picker - For time selection
  • Popover - For calendar dropdown implementations