Skip to content

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

InputsReact

Range Calendar

A month grid for choosing a start and an end date.

Rendered by React 19.3.1

Loading React…
import * as React from 'react'
import { RangeCalendar } from '@hamur/react/range-calendar'

export default function RangeCalendarExample() {
  const [stay, setStay] = React.useState({ start: '2026-11-10', end: '2026-11-13' })
  return (
    <div className="stack">
      <RangeCalendar.Root
        className="calendar"
        aria-label="Stay"
        locale="en-US"
        today="2026-11-07"
        defaultValue={{ start: '2026-11-10', end: '2026-11-13' }}
        onValueChange={(next) => setStay(next)}
      >
        <div className="calendar-header">
          <RangeCalendar.PrevButton className="calendar-nav">‹</RangeCalendar.PrevButton>
          <RangeCalendar.Heading className="calendar-heading" />
          <RangeCalendar.NextButton className="calendar-nav">›</RangeCalendar.NextButton>
        </div>
        <RangeCalendar.Grid className="calendar-grid">
          {(grid) => (
            <>
              <RangeCalendar.GridHeader>
                <RangeCalendar.GridRow>
                  {grid.weekdays.map((weekday) => (
                    <RangeCalendar.HeaderCell
                      key={weekday.index}
                      weekday={weekday.index}
                      className="calendar-weekday"
                    />
                  ))}
                </RangeCalendar.GridRow>
              </RangeCalendar.GridHeader>
              <RangeCalendar.GridBody>
                {grid.weeks.map((week, index) => (
                  <RangeCalendar.GridRow key={index}>
                    {week.map((day) => (
                      <RangeCalendar.Cell key={day.date} date={day.date} className="calendar-cell">
                        <RangeCalendar.Day className="calendar-day" />
                      </RangeCalendar.Cell>
                    ))}
                  </RangeCalendar.GridRow>
                ))}
              </RangeCalendar.GridBody>
            </>
          )}
        </RangeCalendar.Grid>
      </RangeCalendar.Root>
      <p className="muted">
        Check-in {stay.start}, check-out {stay.end}
      </p>
    </div>
  )
}
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:

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

Examples

The demo at the top is the one example this component has so far.

API reference

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

CellRangeCalendar.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.

DayRangeCalendar.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.

GridRangeCalendar.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)—

GridBodyRangeCalendar.GridBody

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

GridHeaderRangeCalendar.GridHeader

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

GridRowRangeCalendar.GridRow

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

HeaderCellRangeCalendar.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—

HeadingRangeCalendar.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

NextButtonRangeCalendar.NextButton

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

PrevButtonRangeCalendar.PrevButton

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

RootRangeCalendar.Root

Groups all parts of a calendar that selects a range of dates. The first press sets the start, the second commits the range; arrows or the pointer preview it and Escape cancels. Values are { start, end } with ISO 8601 strings. Renders a <div>.

Root props
PropTypeDefault
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>—
valueThe selected range, null for none.DateRange | null—
defaultValueThe selected range when initially rendered.DateRange | null—
onValueChangeEvent handler called when a range is committed (the second press).((value: DateRange, eventDetails: CalendarRootChangeEventDetails) => void)—
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
EnterSpaceSets the start of a range; the second press commits itCell
HomeEndPageUpPageDownMove focus as in Calendar; with a start set, the range from the start to the focused day is previewedCell
EscapeCancels the pending range; with none pending it is not consumed, so a popup around it can closeCell
EnterDoes nothing: isDateUnavailable(date, anchor) gets the start, so it can refuse ends that cross a booked nightCell

Accessibility

What the parts set
PartSetsSo that
Rootrole="group", data-rangethe range calendar is one named control
Gridrole="grid", aria-multiselectable="true"the days of the range are read as several selected cells
Cellaria-selected="true" on every day from the start to the end; the preview is not selectiona screen reader reads the committed range, not the pending one
Statusa visually hidden role="status" after the root"From Tuesday, November 10, 2026, choose the end date", the commit and the cancel are announced once, not every arrow key

Focus order

  1. PrevButton, then NextButton, in the order you render them.
  2. The grid is one tab stop, chosen as in Calendar: the day that last had focus, else a selected day, else today.
  3. Arrow keys move focus inside the grid and extend the preview while a start is set.
  4. Tab leaves the grid; the pending start stays until a second press, Escape or a new value.