Headless / Date & time

Calendar

Calendar is a focusable month grid. The arrow keys move day by day, PageUp and PageDown jump a month, and the labels are locale-aware — all without styling.

Loading playground…
Updated 3 days agoEdit on GitHubWeb & native

What it is

A single component — not compound — that renders a role="grid" month view. It manages two pieces of state: the selected date and the focused date. The selected date is what the user chose; the focused date is where keyboard navigation currently sits, and moving it past the month's edge scrolls the grid.

Weekday and month labels come from Intl.DateTimeFormat, so they follow the locale. The renderDay prop replaces the content of each cell, leaving the grid and the keyboard model intact.

Install

example.tsx
yarn add @usemotif/headless
example.tsx
import { Calendar } from '@usemotif/headless';

API

Calendar

stable
function Calendar(props: CalendarProps): JSX.Element
value / defaultValue / onValueChangeDate / Date / (next: Date) => void

The selected date, controlled or uncontrolled.

isDisabled(date: Date) => boolean

Predicate marking individual days disabled — for min/max ranges or blocked dates.

weekStartsOn0 | 1 | 2 | 3 | 4 | 5 | 6= 0

First day of the week — 0 is Sunday, 1 is Monday.

localestring

Locale for weekday and month labels. Defaults to the navigator locale.

renderDay(info) => ReactNode

Custom day-cell renderer. `info` carries `date`, `isSelected`, `isToday`, `isOutsideMonth`, `isDisabled`.

styleCSSProperties

Inline style for the grid.

Keyboard

KeyAction
Arrow Left / RightPrevious / next day
Arrow Up / DownPrevious / next week
PageUp / PageDownPrevious / next month
Home / EndStart / end of the current week
Enter / SpaceSelect the focused day

Accessibility

Calendar is a role="grid" with row, columnheader, and gridcell descendants — the WAI-ARIA grid pattern, so a screen reader announces it as a date grid and reads each cell's full date. Roving tabindex keeps exactly one cell in the Tab order: a keyboard user tabs into the grid once, navigates with the arrow keys, and tabs straight out. Selected cells carry aria-selected; disabled cells carry aria-disabled and are skipped on selection.

Examples

A calendar with a minimum date:

example.tsx
<Calendar
value={date}
onValueChange={setDate}
weekStartsOn={1}
isDisabled={(d) => d < new Date()}
renderDay={({ date, isSelected, isToday }) => (
  <Box
    p="$1"
    borderRadius="$radii.sm"
    bg={isSelected ? '$colors.action.primary.bg' : undefined}
    fontWeight={isToday ? '$bold' : undefined}
  >
    {date.getDate()}
  </Box>
)}
/>