Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

InputsReact

Calendar

A month grid for choosing one date, or several with multiple.

Rendered by React 19.3.1

Loading React…
import { Calendar } from '@hamur/react/calendar'

export default function CalendarExample() {
  return (
    <Calendar.Root
      className="calendar"
      aria-label="Delivery date"
      locale="en-US"
      today="2026-11-07"
      defaultValue="2026-11-12"
    >
      <div className="calendar-header">
        <Calendar.PrevButton className="calendar-nav">
          ‹
        </Calendar.PrevButton>
        <Calendar.Heading className="calendar-heading" />
        <Calendar.NextButton className="calendar-nav">
          ›
        </Calendar.NextButton>
      </div>
      <Calendar.Grid className="calendar-grid">
        {(grid) => (
          <>
            <Calendar.GridHeader>
              <Calendar.GridRow>
                {grid.weekdays.map((weekday) => (
                  <Calendar.HeaderCell
                    key={weekday.index}
                    weekday={weekday.index}
                    className="calendar-weekday"
                  />
                ))}
              </Calendar.GridRow>
            </Calendar.GridHeader>
            <Calendar.GridBody>
              {grid.weeks.map((week, index) => (
                <Calendar.GridRow key={index}>
                  {week.map((day) => (
                    <Calendar.Cell
                      key={day.date}
                      date={day.date}
                      className="calendar-cell"
                    >
                      <Calendar.Day className="calendar-day" />
                    </Calendar.Cell>
                  ))}
                </Calendar.GridRow>
              ))}
            </Calendar.GridBody>
          </>
        )}
      </Calendar.Grid>
    </Calendar.Root>
  )
}
Not measured yetTests passing in React
Not measured yetSize
Not measured yetMemory
Not measured yetSpeed
[N] commits

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<Calendar.Root>
  <Calendar.PrevButton />
  <Calendar.Heading />
  <Calendar.NextButton />
  <Calendar.Grid>
    <Calendar.GridHeader>
      <Calendar.GridRow>
        <Calendar.HeaderCell />
      </Calendar.GridRow>
    </Calendar.GridHeader>
    <Calendar.GridBody>
        <Calendar.Cell>
          <Calendar.Day />
        </Calendar.Cell>
    </Calendar.GridBody>
  </Calendar.Grid>
</Calendar.Root>

Examples

Limits

min and max disable the days outside November 3 to December 18 and the Previous and Next buttons at the bounds; isDateUnavailable books the weekends, which take focus but cannot be chosen. The rule is in text, linked with aria-describedby.

CalendarLimits.tsx
import * as React from 'react'
import { Calendar } from '@hamur/react/calendar'

// Saturday and Sunday are booked: they can hold focus but not be chosen.
const isWeekend = (date: string) => {
  const day = new Date(`${date}T00:00:00Z`).getUTCDay()
  return day === 0 || day === 6
}

export default function CalendarLimitsExample() {
  const [value, setValue] = React.useState<string | null>(null)
  return (
    <div className="stack">
      <Calendar.Root
        className="calendar"
        aria-label="Appointment"
        aria-describedby="calendar-limits-rule"
        locale="en-US"
        today="2026-11-07"
        min="2026-11-03"
        max="2026-12-18"
        isDateUnavailable={isWeekend}
        onValueChange={(next) => setValue(next)}
      >
        <div className="calendar-header">
          <Calendar.PrevButton className="calendar-nav">‹</Calendar.PrevButton>
          <Calendar.Heading className="calendar-heading" />
          <Calendar.NextButton className="calendar-nav">›</Calendar.NextButton>
        </div>
        <Calendar.Grid className="calendar-grid">
          {(grid) => (
            <>
              <Calendar.GridHeader>
                <Calendar.GridRow>
                  {grid.weekdays.map((weekday) => (
                    <Calendar.HeaderCell
                      key={weekday.index}
                      weekday={weekday.index}
                      className="calendar-weekday"
                    />
                  ))}
                </Calendar.GridRow>
              </Calendar.GridHeader>
              <Calendar.GridBody>
                {grid.weeks.map((week, index) => (
                  <Calendar.GridRow key={index}>
                    {week.map((day) => (
                      <Calendar.Cell key={day.date} date={day.date} className="calendar-cell">
                        <Calendar.Day className="calendar-day" />
                      </Calendar.Cell>
                    ))}
                  </Calendar.GridRow>
                ))}
              </Calendar.GridBody>
            </>
          )}
        </Calendar.Grid>
      </Calendar.Root>
      <p id="calendar-limits-rule" className="muted">
        Weekdays from November 3 to December 18. Weekends are booked.
      </p>
      <p className="muted">Appointment: {value ?? 'none'}</p>
    </div>
  )
}

Multiple

multiple toggles days and reports them in ascending order; maxSelected stops at five.

CalendarMultiple.tsx
import * as React from 'react'
import { Calendar } from '@hamur/react/calendar'

export default function CalendarMultipleExample() {
  const [value, setValue] = React.useState<string[]>(['2026-11-10', '2026-11-12'])
  return (
    <div className="stack">
      <Calendar.Root
        className="calendar"
        aria-label="Workout days"
        locale="en-US"
        today="2026-11-07"
        multiple
        maxSelected={5}
        defaultValue={['2026-11-10', '2026-11-12']}
        onValueChange={(next) => setValue(next)}
      >
        <div className="calendar-header">
          <Calendar.PrevButton className="calendar-nav">‹</Calendar.PrevButton>
          <Calendar.Heading className="calendar-heading" />
          <Calendar.NextButton className="calendar-nav">›</Calendar.NextButton>
        </div>
        <Calendar.Grid className="calendar-grid">
          {(grid) => (
            <>
              <Calendar.GridHeader>
                <Calendar.GridRow>
                  {grid.weekdays.map((weekday) => (
                    <Calendar.HeaderCell
                      key={weekday.index}
                      weekday={weekday.index}
                      className="calendar-weekday"
                    />
                  ))}
                </Calendar.GridRow>
              </Calendar.GridHeader>
              <Calendar.GridBody>
                {grid.weeks.map((week, index) => (
                  <Calendar.GridRow key={index}>
                    {week.map((day) => (
                      <Calendar.Cell key={day.date} date={day.date} className="calendar-cell">
                        <Calendar.Day className="calendar-day" />
                      </Calendar.Cell>
                    ))}
                  </Calendar.GridRow>
                ))}
              </Calendar.GridBody>
            </>
          )}
        </Calendar.Grid>
      </Calendar.Root>
      <p className="muted">
        Selected ({value.length} of 5): {value.join(', ') || 'none'}
      </p>
    </div>
  )
}

Two Months

visibleMonths={2} with a Heading and a Grid per offset; Previous and Next move both months, and a date the other month shows is a hidden, empty cell.

CalendarTwoMonths.tsx
import { Calendar } from '@hamur/react/calendar'

export default function CalendarTwoMonthsExample() {
  return (
    <Calendar.Root
      className="calendar"
      aria-label="Departure"
      locale="en-US"
      today="2026-11-07"
      visibleMonths={2}
      defaultValue="2026-11-20"
    >
      <div className="calendar-header">
        <Calendar.PrevButton className="calendar-nav">‹</Calendar.PrevButton>
        <Calendar.NextButton className="calendar-nav">›</Calendar.NextButton>
      </div>
      <div className="calendar-months">
        {[0, 1].map((offset) => (
          <div key={offset} className="calendar-month">
            <Calendar.Heading offset={offset} className="calendar-heading" />
            <Calendar.Grid offset={offset} className="calendar-grid">
              {(grid) => (
                <>
                  <Calendar.GridHeader>
                    <Calendar.GridRow>
                      {grid.weekdays.map((weekday) => (
                        <Calendar.HeaderCell
                          key={weekday.index}
                          weekday={weekday.index}
                          className="calendar-weekday"
                        />
                      ))}
                    </Calendar.GridRow>
                  </Calendar.GridHeader>
                  <Calendar.GridBody>
                    {grid.weeks.map((week, index) => (
                      <Calendar.GridRow key={index}>
                        {week.map((day) => (
                          <Calendar.Cell key={day.date} date={day.date} className="calendar-cell">
                            <Calendar.Day className="calendar-day" />
                          </Calendar.Cell>
                        ))}
                      </Calendar.GridRow>
                    ))}
                  </Calendar.GridBody>
                </>
              )}
            </Calendar.Grid>
          </div>
        ))}
      </div>
    </Calendar.Root>
  )
}

Form

Inside Field and Form: required blocks the submit until a day is chosen, and the date submits as arrival=2026-11-12 from a hidden input type="date".

CalendarForm.tsx
import * as React from 'react'
import { Calendar } from '@hamur/react/calendar'
import { Field } from '@hamur/react/field'
import { Form } from '@hamur/react/form'

export default function CalendarFormExample() {
  const [submitted, setSubmitted] = React.useState('')
  return (
    <Form
      className="stack"
      onFormSubmit={(_values, details) => {
        const form = details.event.target as HTMLFormElement
        setSubmitted([...new FormData(form)].map(([key, value]) => `${key}=${value}`).join('&'))
      }}
    >
      <Field.Root name="arrival" className="field">
        <Field.Label className="calendar-label">Arrival</Field.Label>
        <Calendar.Root className="calendar" locale="en-US" today="2026-11-07" required>
          <div className="calendar-header">
            <Calendar.PrevButton className="calendar-nav">‹</Calendar.PrevButton>
            <Calendar.Heading className="calendar-heading" />
            <Calendar.NextButton className="calendar-nav">›</Calendar.NextButton>
          </div>
          <Calendar.Grid className="calendar-grid">
            {(grid) => (
              <>
                <Calendar.GridHeader>
                  <Calendar.GridRow>
                    {grid.weekdays.map((weekday) => (
                      <Calendar.HeaderCell
                        key={weekday.index}
                        weekday={weekday.index}
                        className="calendar-weekday"
                      />
                    ))}
                  </Calendar.GridRow>
                </Calendar.GridHeader>
                <Calendar.GridBody>
                  {grid.weeks.map((week, index) => (
                    <Calendar.GridRow key={index}>
                      {week.map((day) => (
                        <Calendar.Cell key={day.date} date={day.date} className="calendar-cell">
                          <Calendar.Day className="calendar-day" />
                        </Calendar.Cell>
                      ))}
                    </Calendar.GridRow>
                  ))}
                </Calendar.GridBody>
              </>
            )}
          </Calendar.Grid>
        </Calendar.Root>
        <Field.Error className="error" />
      </Field.Root>
      <button className="button" type="submit">
        Book
      </button>
      {submitted ? <pre className="output">{submitted}</pre> : null}
    </Form>
  )
}

Locale

ar-EG in right to left (Arabic-Indic digits, weeks from Saturday, the arrow keys swapped), with a switcher to de-DE and en-US and firstDayOfWeek to start on Monday.

CalendarLocale.tsx
import * as React from 'react'
import { Calendar } from '@hamur/react/calendar'

const locales = [
  { locale: 'en-US', dir: 'ltr', weekStart: 'Sunday' },
  { locale: 'de-DE', dir: 'ltr', weekStart: 'Monday' },
  { locale: 'ar-EG', dir: 'rtl', weekStart: 'Saturday' },
] as const

export default function CalendarLocaleExample() {
  const [locale, setLocale] = React.useState<string>('ar-EG')
  const [monday, setMonday] = React.useState(false)
  const current = locales.find((entry) => entry.locale === locale)!
  return (
    <div className="stack">
      <div className="calendar-locales" role="group" aria-label="Locale">
        {locales.map((entry) => (
          <button
            key={entry.locale}
            type="button"
            className="calendar-chip"
            aria-pressed={entry.locale === locale}
            onClick={() => setLocale(entry.locale)}
          >
            {entry.locale}
          </button>
        ))}
      </div>
      <label className="row">
        <input
          type="checkbox"
          checked={monday}
          onChange={(event) => setMonday(event.target.checked)}
        />
        firstDayOfWeek=1 (Monday)
      </label>
      <Calendar.Root
        className="calendar"
        aria-label="Date"
        dir={current.dir}
        locale={locale}
        firstDayOfWeek={monday ? 1 : undefined}
        today="2026-11-07"
        defaultValue="2026-11-12"
      >
        <div className="calendar-header">
          <Calendar.PrevButton className="calendar-nav">‹</Calendar.PrevButton>
          <Calendar.Heading className="calendar-heading" />
          <Calendar.NextButton className="calendar-nav">›</Calendar.NextButton>
        </div>
        <Calendar.Grid className="calendar-grid">
          {(grid) => (
            <>
              <Calendar.GridHeader>
                <Calendar.GridRow>
                  {grid.weekdays.map((weekday) => (
                    <Calendar.HeaderCell
                      key={weekday.index}
                      weekday={weekday.index}
                      className="calendar-weekday"
                    />
                  ))}
                </Calendar.GridRow>
              </Calendar.GridHeader>
              <Calendar.GridBody>
                {grid.weeks.map((week, index) => (
                  <Calendar.GridRow key={index}>
                    {week.map((day) => (
                      <Calendar.Cell key={day.date} date={day.date} className="calendar-cell">
                        <Calendar.Day className="calendar-day" />
                      </Calendar.Cell>
                    ))}
                  </Calendar.GridRow>
                ))}
              </Calendar.GridBody>
            </>
          )}
        </Calendar.Grid>
      </Calendar.Root>
      <p className="muted">
        {locale}, {current.dir === 'rtl' ? 'right to left' : 'left to right'}, weeks start on{' '}
        {monday ? 'Monday' : current.weekStart}
      </p>
    </div>
  )
}

Controlled Month

visibleMonth and onVisibleMonthChange keep the month in state; buttons outside set it, and the readout shows the last details.reason (pointer, keyboard or none).

CalendarControlledMonth.tsx
import * as React from 'react'
import { Calendar } from '@hamur/react/calendar'

// A month as the calendar takes it: `YYYY-MM`.
type Month = `${number}-${number}`

export default function CalendarControlledMonthExample() {
  const [month, setMonth] = React.useState<Month>('2026-11')
  const [reason, setReason] = React.useState('none')
  return (
    <div className="stack">
      <div className="button-row">
        <button className="button" type="button" onClick={() => setMonth('2026-11')}>
          This month
        </button>
        <button className="button" type="button" onClick={() => setMonth('2027-03')}>
          March 2027
        </button>
      </div>
      <Calendar.Root
        className="calendar"
        aria-label="Date"
        locale="en-US"
        today="2026-11-07"
        visibleMonth={month}
        onVisibleMonthChange={(next, details) => {
          setMonth(next)
          setReason(details.reason)
        }}
      >
        <div className="calendar-header">
          <Calendar.PrevButton className="calendar-nav">‹</Calendar.PrevButton>
          <Calendar.Heading className="calendar-heading" />
          <Calendar.NextButton className="calendar-nav">›</Calendar.NextButton>
        </div>
        <Calendar.Grid className="calendar-grid">
          {(grid) => (
            <>
              <Calendar.GridHeader>
                <Calendar.GridRow>
                  {grid.weekdays.map((weekday) => (
                    <Calendar.HeaderCell
                      key={weekday.index}
                      weekday={weekday.index}
                      className="calendar-weekday"
                    />
                  ))}
                </Calendar.GridRow>
              </Calendar.GridHeader>
              <Calendar.GridBody>
                {grid.weeks.map((week, index) => (
                  <Calendar.GridRow key={index}>
                    {week.map((day) => (
                      <Calendar.Cell key={day.date} date={day.date} className="calendar-cell">
                        <Calendar.Day className="calendar-day" />
                      </Calendar.Cell>
                    ))}
                  </Calendar.GridRow>
                ))}
              </Calendar.GridBody>
            </>
          )}
        </Calendar.Grid>
      </Calendar.Root>
      <p className="muted">
        Month: {month}. Last reason: {reason}
      </p>
    </div>
  )
}

API reference

34 props across 11 parts, in React syntax. className, style, render are listed once, under Shared props.

CellCalendar.Cell

A cell of a week, and the focus holder of its date (APG): it is the roving tab stop, carries aria-selected, aria-disabled and aria-current, is named by the full date, and is pressed by click, Enter and Space. Put a Calendar.Day inside for styling. Renders a <td>.

Cell props
PropTypeDefault
daterequiredThe date of the cell, '2026-11-07'.`${number}-${number}-${number}`—
Cell data attributes
Data attributeDescription
data-selectedPresent when the date is selected (every date of a selected range).
data-todayPresent on today's date.
data-disabledPresent when the date cannot be focused or chosen: before min, after max, or disabled.
data-unavailablePresent when the date can be focused but not chosen (isDateUnavailable).
data-outside-monthPresent on a date that belongs to the previous or the next month.
data-weekendPresent on a date of the locale's weekend.
data-highlightedPresent when the date has focus.
data-range-startRange: present on the first date of the selected range.
data-range-endRange: present on the last date of the selected range.
data-in-rangeRange: present on a date between the ends of the selected range.
data-previewRange: present on the dates of the pending range (anchor to hovered or focused date).
data-preview-startRange: present on the first date of the pending range.
data-preview-endRange: present on the last date of the pending range.

DayCalendar.Day

The content of a Calendar.Cell: the day number, for styling. It is presentational (no role, no tab stop): the cell holds focus and the state. It carries the same data-* attributes as the cell and renders nothing for an outside day that another visible month shows. Renders a <span>.

Day data attributes
Data attributeDescription
data-selectedPresent when the date is selected (every date of a selected range).
data-todayPresent on today's date.
data-disabledPresent when the date cannot be focused or chosen: before min, after max, or disabled.
data-unavailablePresent when the date can be focused but not chosen (isDateUnavailable).
data-outside-monthPresent on a date that belongs to the previous or the next month.
data-weekendPresent on a date of the locale's weekend.
data-highlightedPresent when the date has focus.
data-range-startRange: present on the first date of the selected range.
data-range-endRange: present on the last date of the selected range.
data-in-rangeRange: present on a date between the ends of the selected range.
data-previewRange: present on the dates of the pending range (anchor to hovered or focused date).
data-preview-startRange: present on the first date of the pending range.
data-preview-endRange: present on the last date of the pending range.

GridCalendar.Grid

The grid of one month. Its children are a function of the month: weeks (the days of each week), weekdays (the header cells, starting with the first day of the week), month and label. Renders a <table>.

Grid props
PropTypeDefault
offsetWhich of the visible months this grid shows: 0 is the first.number0
childrenThe content, or a function of the month's weeks and weekdays.React.ReactNode | ((grid: CalendarGrid) => React.ReactNode)—

GridBodyCalendar.GridBody

The body of a grid: the rows of weeks. Renders a <tbody>.

GridHeaderCalendar.GridHeader

The header of a grid: put a Calendar.GridRow of Calendar.HeaderCell in it. Renders a <thead>.

GridRowCalendar.GridRow

A row of a grid: a week, or the row of weekday names. Renders a <tr>.

HeaderCellCalendar.HeaderCell

The name of a weekday above its column. abbr is the long name. Renders a <th>.

HeaderCell props
PropTypeDefault
weekdayrequiredThe weekday of the column: 0 is Sunday, 6 is Saturday.number—

HeadingCalendar.Heading

The name of a visible month, for example "November 2026", and the label of its grid. The first one is a polite live region, so a screen reader announces a new month. Renders a <h2>.

Heading props
PropTypeDefault
offsetWhich of the visible months this heading names: 0 is the first.number0

NextButtonCalendar.NextButton

Shows the next month(s). It is aria-disabled, not disabled, at the max bound, so it keeps focus. Renders a <button>.

PrevButtonCalendar.PrevButton

Shows the previous month(s). It is aria-disabled, not disabled, at the min bound, so it keeps focus. Renders a <button>.

RootCalendar.Root

Groups all parts of a calendar that selects one date, or several with multiple. Values are ISO 8601 strings such as '2026-11-07'. Renders a <div>.

Root props
PropTypeDefault
valueThe selected date, null for none.CalendarInputValue<Multiple> | null—
defaultValueThe selected date when initially rendered.CalendarInputValue<Multiple> | null—
onValueChangeEvent handler called when the selection changes.((value: CalendarOutputValue<Multiple>, eventDetails: CalendarRootChangeEventDetails) => void)—
multipleWhether several dates can be selected.Multiplefalse
minThe earliest date that can be focused or chosen ('2026-11-07').`${number}-${number}-${number}`—
maxThe latest date that can be focused or chosen.`${number}-${number}-${number}`—
isDateUnavailableWhether a date can be focused but not chosen.((date: `${number}-${number}-${number}`, anchor: `${number}-${number}-${number}` | null) => boolean)—
isDateDisabledWhether a date is disabled: navigation skips it and it cannot be chosen.((date: `${number}-${number}-${number}`) => boolean)—
disabledWhether the component should ignore user interaction.booleanfalse
readOnlyWhether the user should be unable to change the selection.booleanfalse
requiredWhether the user must choose a date before submitting a form.booleanfalse
nameIdentifies the field when a form is submitted.string—
formIdentifies the form that owns the hidden input.string—
localeThe BCP 47 locale of the month names, weekday names, digits and first day of the week.string'en-US'
firstDayOfWeekThe first day of the week, 0 (Sunday) to 6 (Saturday).number—
visibleMonthThe first visible month ('2026-11' or a date).`${number}-${number}-${number}` | `${number}-${number}` | null—
defaultVisibleMonthThe first visible month when initially rendered.`${number}-${number}-${number}` | `${number}-${number}`—
onVisibleMonthChangeEvent handler called when the first visible month changes.((month: `${number}-${number}`, eventDetails: CalendarRootVisibleMonthChangeEventDetails) => void)—
visibleMonthsThe number of months shown.number1
pageBehaviorWhether Previous and Next move by the number of visible months or by one.CalendarPageBehavior'visible'
fixedWeeksWhether every month shows six weeks.booleanfalse
todayThe date of today ('2026-11-07').`${number}-${number}-${number}`—
timeZoneThe time zone that decides what today is, when today is not given.string—
highlightedDateThe date that holds focus.`${number}-${number}-${number}` | null—
onHighlightChangeEvent handler called when the focused date changes.((date: `${number}-${number}-${number}` | null) => void)—
disallowEmptySelectionWhether the last selected date cannot be deselected.booleanfalse
maxSelectedmultiple only: the most dates that can be selected.number—
labelsThe texts the calendar adds: the Previous and Next labels and the words after a date's name.Partial<CalendarLabels>—
inputRefA ref to access the hidden input element.React.Ref<HTMLInputElement>—
Root data attributes
Data attributeDescription
data-disabledPresent when the calendar is disabled.
data-readonlyPresent when the calendar does not allow changing the selection.
data-requiredPresent when the calendar is required.
data-multiplePresent when the calendar allows several selected dates.
data-rangePresent in a range calendar.
data-emptyPresent when nothing is selected.

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

Keyboard
KeysDoesPart
ArrowLeftArrowRightMoves focus a day, without selecting; past the first or last day it shows the next monthCell
ArrowUpArrowDownMoves focus a weekCell
HomeEndMoves focus to the first or last day of the locale's week (or of firstDayOfWeek)Cell
PageUpPageDownMoves a month, keeping the day number or the last day of a shorter month; with Shift, a yearCell
EnterSpaceSelects the focused day; with multiple, toggles it; an unavailable day is not chosenCell
ArrowLeftArrowRightSwapped: ArrowLeft moves to the next day; Home and End stay on the locale's weekCell
minmaxStay inside the bounds and skip disabled days; unavailable days still take focusCell
TabEnters on the day that last had focus, else the selected day, else today, else the first enabled dayGrid
EnterSpaceShows the previous or next month (reason: 'keyboard'); at a bound the button keeps focus and does nothingPrevButton

Accessibility

What the parts set
PartSetsSo that
Rootrole="group", named by aria-label or Field.Labelthe calendar is one named control; no aria-invalid on the group, the Field error says what is wrong
Headingh2, aria-live="polite" and aria-atomic on the first onea new month is announced when the buttons or the keys change it, and not on the first render
Gridrole="grid", aria-labelledby its Heading (aria-label with the month as the fallback), aria-multiselectableeach month is read by its name, and whether several days can be chosen
HeaderCellscope="col", the short name as text and the long name as aria-label and abbrthe weekday column is read as "Monday", not "Mo"
Cellrole="gridcell", roving tabindex, aria-selected, aria-disabled, aria-current="date", aria-label the full datethe focused element carries the state, and is read "Thursday, November 12, 2026, selected"
Daynothing: no role and no tab stopthe number inside the cell only styles; the cell's name already contains it
PrevButton, NextButtonaria-label ("Previous month", "Next month" from labels), aria-disabled at the bound, never disabledthe buttons keep focus at min or max and say they do nothing

Focus order

  1. PrevButton, then NextButton, in the order you render them.
  2. The grid is one tab stop: the day that last had focus when it is still visible, else the selected day, else today, else the first enabled day. Nothing takes focus on mount.
  3. Arrow keys, Home, End, PageUp and PageDown move focus inside the grid, skip disabled days and change the visible month when they leave it. Keys are handled only on a cell; there are no document-level listeners.
  4. Tab leaves the grid.