Skip to main content

Calendar

Compound components for building accessible calendars, powered by @gentleduck/calendar.

import * as Calendar from '@gentleduck/primitives/calendar'
import * as Calendar from '@gentleduck/primitives/calendar'

Anatomy

<Calendar.Root>
  <Calendar.Header />
  <Calendar.Nav>
    <Calendar.PrevButton />
    <Calendar.NextButton />
  </Calendar.Nav>
  <Calendar.Grid>
    <Calendar.Weekdays />
    {/* Map over state.weeks to render Calendar.Day */}
  </Calendar.Grid>
  <Calendar.MonthView />
  <Calendar.YearView />
</Calendar.Root>
<Calendar.Root>
  <Calendar.Header />
  <Calendar.Nav>
    <Calendar.PrevButton />
    <Calendar.NextButton />
  </Calendar.Nav>
  <Calendar.Grid>
    <Calendar.Weekdays />
    {/* Map over state.weeks to render Calendar.Day */}
  </Calendar.Grid>
  <Calendar.MonthView />
  <Calendar.YearView />
</Calendar.Root>

Example

import { NativeAdapter } from '@gentleduck/calendar'
import * as Calendar from '@gentleduck/primitives/calendar'
 
const adapter = new NativeAdapter()
 
function DatePicker() {
  return (
    <Calendar.Root adapter={adapter} mode="single" className="p-4 border rounded-lg">
      <div className="flex items-center justify-between mb-4">
        <Calendar.Header className="text-sm font-medium" />
        <Calendar.Nav className="flex gap-1">
          <Calendar.PrevButton className="h-7 w-7 rounded hover:bg-accent">
            <-
          </Calendar.PrevButton>
          <Calendar.NextButton className="h-7 w-7 rounded hover:bg-accent">
            ->
          </Calendar.NextButton>
        </Calendar.Nav>
      </div>
      <Calendar.Grid className="grid grid-cols-7 gap-1">
        <Calendar.Weekdays className="contents text-muted-foreground text-xs" />
        {/* Day cells rendered via context */}
      </Calendar.Grid>
    </Calendar.Root>
  )
}
import { NativeAdapter } from '@gentleduck/calendar'
import * as Calendar from '@gentleduck/primitives/calendar'
 
const adapter = new NativeAdapter()
 
function DatePicker() {
  return (
    <Calendar.Root adapter={adapter} mode="single" className="p-4 border rounded-lg">
      <div className="flex items-center justify-between mb-4">
        <Calendar.Header className="text-sm font-medium" />
        <Calendar.Nav className="flex gap-1">
          <Calendar.PrevButton className="h-7 w-7 rounded hover:bg-accent">
            <-
          </Calendar.PrevButton>
          <Calendar.NextButton className="h-7 w-7 rounded hover:bg-accent">
            ->
          </Calendar.NextButton>
        </Calendar.Nav>
      </div>
      <Calendar.Grid className="grid grid-cols-7 gap-1">
        <Calendar.Weekdays className="contents text-muted-foreground text-xs" />
        {/* Day cells rendered via context */}
      </Calendar.Grid>
    </Calendar.Root>
  )
}

API

Calendar.Root

The root component. Wraps useCalendar and provides context to all children. Renders a div with role="application".

PropTypeDefaultDescription
adapterAdapter.IDateAdapter<Date>requiredDate adapter instance
modeSelection.SelectionModerequired'single', 'range', 'multi', or 'multi-range'
localeCalendarLocaleConfig-Locale, week start day, and direction
monthDate-Controlled displayed month
defaultMonthDate-Default month (uncontrolled)
selectedCalendarValue<Date, M>-Controlled selection
defaultSelectedCalendarValue<Date, M>-Default selection (uncontrolled)
onSelect(value) => void-Called when selection changes
onMonthChange(month: Date) => void-Called when displayed month changes
onDismiss() => void-Called on Escape
numberOfMonthsnumber1Months to show side by side
showOutsideDaysbooleantrueShow days from adjacent months
fixedWeeksbooleanfalseAlways show 6 weeks
disabledDate[] | (date) => boolean-Disabled dates
fromDateDate-Earliest selectable date
toDateDate-Latest selectable date

Data attributes:

AttributeValue
data-slot"calendar"
data-viewCurrent ViewMode ("days", "months", or "years")

Calendar.Header

Displays the current month and year. Renders a div with aria-live="polite".

PropTypeDefaultDescription
formatMonth(month: Date, adapter: Adapter.IDateAdapter<Date>) => string-Custom formatter for the title. Defaults to "March 2026" format

Data attributes:

AttributeValue
data-slot"calendar-header"

Calendar.Nav

Navigation wrapper. Renders a div with role="navigation". When no children are provided, it renders PrevButton and NextButton automatically.

Data attributes:

AttributeValue
data-slot"calendar-nav"

Calendar.PrevButton

Navigates to the previous month. Renders a button.

PropTypeDefaultDescription
All button props--Standard button HTML attributes

Automatically applies aria-label="Go to previous month" and disabled from context.

Data attributes:

AttributeValue
data-slot"calendar-nav-button"
data-direction"prev"

Calendar.NextButton

Navigates to the next month. Renders a button.

PropTypeDefaultDescription
All button props--Standard button HTML attributes

Automatically applies aria-label="Go to next month" and disabled from context.

Data attributes:

AttributeValue
data-slot"calendar-nav-button"
data-direction"next"

Calendar.Grid

The day grid container. Renders a div with role="grid", aria-roledescription="calendar", and aria-labelledby pointing to the Header element (set automatically by getGridProps()).

Data attributes:

AttributeValue
data-slot"calendar-grid"

Calendar.Weekdays

Renders the weekday header row as a div with role="row". Each weekday renders as an abbr with role="columnheader".

PropTypeDefaultDescription
renderWeekday(weekday: string, index: number) => ReactNode-Custom render function for each weekday cell

Data attributes:

AttributeValue
data-slot"calendar-weekdays"
data-slot (each cell)"calendar-weekday"

Calendar.Day

A single day cell. Renders a button with full ARIA attributes and data attributes for styling.

PropTypeDefaultDescription
dayGrid.ICalendarDay<Date>requiredDay data from the calendar grid

Automatically applies all day props from context: aria-label, aria-selected, aria-disabled, aria-current, tabIndex, onClick, onKeyDown, onMouseEnter, and all data-* styling attributes.

Data attributes:

AttributeValue
data-slot"calendar-day"
data-calendar-day"" - marker attribute used internally for focus queries
data-selected"true" when selected
data-today"true" for today
data-disabled"true" when disabled
data-outside-month"true" for adjacent month days
data-hidden"true" when showOutsideDays is false and day is outside
data-range-start"true" for range start
data-range-middle"true" for range middle
data-range-end"true" for range end
data-focused"true" when keyboard focused
data-weekend"true" for weekends

Calendar.MonthView

A 12-month grid for month picking. Renders a div with role="grid". Clicking a month navigates to it and switches back to the day view.

Data attributes:

AttributeValue
data-slot"calendar-month-view"
data-slot (each cell)"calendar-month"
data-current"true" on the current month
data-month (each cell)Zero-based month index (e.g. 0 for January)

Calendar.YearView

A decade year grid for year picking. Renders a div with role="grid". Clicking a year navigates to it and switches to the month view.

Data attributes:

AttributeValue
data-slot"calendar-year-view"
data-slot (each cell)"calendar-year"
data-current"true" on the current year
data-year (each cell)Four-digit year (e.g. 2026)

Accessibility

  • Calendar.Root renders role="application" with aria-label="Calendar"
  • Calendar.Grid renders role="grid" with aria-roledescription="calendar" and aria-labelledby linked to the Header
  • Calendar.Weekdays renders role="row" with each weekday cell as role="columnheader"
  • Calendar.Day renders role="gridcell" with full ARIA labeling
  • Calendar.MonthView and Calendar.YearView render each button cell with role="gridcell"
  • Calendar.Nav renders role="navigation" with aria-label="Calendar navigation"
  • Screen reader announcements are rendered automatically via AnnouncerPortal
  • Keyboard navigation follows the WAI-ARIA grid pattern (ArrowLeft/ArrowRight, PageUp/PageDown, Home/End, Enter/Space, Escape)