Skip to content

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

InputsReact

Week Calendar

A month grid for choosing a whole week.

Rendered by React 19.3.1

Loading React…
import { Field } from '@hamur/react/field'
import { WeekCalendar } from '@hamur/react/week-calendar'

export default function WeekCalendarExample() {
  return (
    <Field.Root className="field">
      <Field.Label className="calendar-label">Reporting week</Field.Label>
      <WeekCalendar.Root
        className="calendar"
        locale="en-US"
        today="2026-11-11"
        defaultValue="2026-11-11"
      >
        <div className="calendar-header">
          <WeekCalendar.PrevButton className="calendar-nav">
            ‹
          </WeekCalendar.PrevButton>
          <WeekCalendar.Heading className="calendar-heading" />
          <WeekCalendar.NextButton className="calendar-nav">
            ›
          </WeekCalendar.NextButton>
        </div>
        <WeekCalendar.Grid className="calendar-grid week-calendar-grid">
          {(grid) => (
            <>
              <WeekCalendar.GridHeader>
                <WeekCalendar.GridRow>
                  <WeekCalendar.WeekNumberHeaderCell
                    className="calendar-weekday"
                  >
                    Wk
                  </WeekCalendar.WeekNumberHeaderCell>
                  {grid.weekdays.map((weekday) => (
                    <WeekCalendar.HeaderCell
                      key={weekday.index}
                      weekday={weekday.index}
                      className="calendar-weekday"
                    />
                  ))}
                </WeekCalendar.GridRow>
              </WeekCalendar.GridHeader>
              <WeekCalendar.GridBody>
                {grid.weeks.map(({ week, days }) => (
                  <WeekCalendar.Row
                    key={week.start}
                    value={week.start}
                    className="week-calendar-row"
                  >
                    <WeekCalendar.WeekNumber
                      className="week-calendar-number"
                    />
                    {days.map((day) => (
                      <WeekCalendar.Cell
                        key={day.date}
                        date={day.date}
                        className="week-calendar-cell"
                      >
                        <WeekCalendar.Day className="calendar-day" />
                      </WeekCalendar.Cell>
                    ))}
                  </WeekCalendar.Row>
                ))}
              </WeekCalendar.GridBody>
            </>
          )}
        </WeekCalendar.Grid>
      </WeekCalendar.Root>
    </Field.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:

<WeekCalendar.Root>
  <WeekCalendar.PrevButton />
  <WeekCalendar.Heading />
  <WeekCalendar.NextButton />
  <WeekCalendar.Grid>
    <WeekCalendar.GridHeader>
      <WeekCalendar.GridRow>
        <WeekCalendar.WeekNumberHeaderCell />
        <WeekCalendar.HeaderCell />
      </WeekCalendar.GridRow>
    </WeekCalendar.GridHeader>
    <WeekCalendar.GridBody>
      <WeekCalendar.Row>
        <WeekCalendar.WeekNumber />
        <WeekCalendar.Cell>
          <WeekCalendar.Day />
        </WeekCalendar.Cell>
      </WeekCalendar.Row>
    </WeekCalendar.GridBody>
  </WeekCalendar.Grid>
</WeekCalendar.Root>

Examples

Iso

weekRule switched between 'iso' and 'locale' in January 2027: the same day is in week 53 of 2026 or week 1 of 2027, read with getWeekOfDate and written with formatWeekString.

WeekCalendarIso.tsx
import * as React from 'react'
import type { DateString } from '@hamur/core'
import { WeekCalendar } from '@hamur/react/week-calendar'

const rules = [
  { rule: 'locale', label: 'en-US', text: 'Weeks start on Sunday; week 1 holds January 1.' },
  {
    rule: 'iso',
    label: 'ISO 8601',
    text: 'Weeks start on Monday; week 1 holds the first Thursday.',
  },
] as const

export default function WeekCalendarIsoExample() {
  const [rule, setRule] = React.useState<'locale' | 'iso'>('iso')
  const [value, setValue] = React.useState<DateString | null>('2027-01-01')
  const chosen = value
    ? WeekCalendar.getWeekOfDate(value, WeekCalendar.getWeekRule('en-US', { weekRule: rule }))
    : null
  return (
    <div className="stack">
      <div className="calendar-locales" role="group" aria-label="Week rule">
        {rules.map((entry) => (
          <button
            key={entry.rule}
            type="button"
            className="calendar-chip"
            aria-pressed={entry.rule === rule}
            onClick={() => setRule(entry.rule)}
          >
            {entry.label}
          </button>
        ))}
      </div>
      <p className="muted">{rules.find((entry) => entry.rule === rule)!.text}</p>
      <WeekCalendar.Root
        className="calendar"
        aria-label="Billing week"
        locale="en-US"
        weekRule={rule}
        today="2026-11-11"
        defaultVisibleMonth="2027-01"
        value={value}
        onValueChange={(next) => setValue(next)}
      >
        <div className="calendar-header">
          <WeekCalendar.PrevButton className="calendar-nav">‹</WeekCalendar.PrevButton>
          <WeekCalendar.Heading className="calendar-heading" />
          <WeekCalendar.NextButton className="calendar-nav">›</WeekCalendar.NextButton>
        </div>
        <WeekCalendar.Grid className="calendar-grid week-calendar-grid">
          {(grid) => (
            <>
              <WeekCalendar.GridHeader>
                <WeekCalendar.GridRow>
                  <WeekCalendar.WeekNumberHeaderCell className="calendar-weekday">
                    Wk
                  </WeekCalendar.WeekNumberHeaderCell>
                  {grid.weekdays.map((weekday) => (
                    <WeekCalendar.HeaderCell
                      key={weekday.index}
                      weekday={weekday.index}
                      className="calendar-weekday"
                    />
                  ))}
                </WeekCalendar.GridRow>
              </WeekCalendar.GridHeader>
              <WeekCalendar.GridBody>
                {grid.weeks.map(({ week, days }) => (
                  <WeekCalendar.Row
                    key={week.start}
                    value={week.start}
                    className="week-calendar-row"
                  >
                    <WeekCalendar.WeekNumber className="week-calendar-number" />
                    {days.map((day) => (
                      <WeekCalendar.Cell
                        key={day.date}
                        date={day.date}
                        className="week-calendar-cell"
                      >
                        <WeekCalendar.Day className="calendar-day" />
                      </WeekCalendar.Cell>
                    ))}
                  </WeekCalendar.Row>
                ))}
              </WeekCalendar.GridBody>
            </>
          )}
        </WeekCalendar.Grid>
      </WeekCalendar.Root>
      <p className="muted">
        Week:{' '}
        {chosen
          ? `${WeekCalendar.formatWeekString(chosen)}, ${chosen.start} to ${chosen.end}`
          : 'none'}
      </p>
    </div>
  )
}

Limits

min and max disable the weeks outside November 8 to December 13 and the buttons at the bounds; isWeekUnavailable books one week, which takes focus but cannot be chosen.

WeekCalendarLimits.tsx
import * as React from 'react'
import { WeekCalendar } from '@hamur/react/week-calendar'

const isBooked = (week: { start: string }) => week.start === '2026-11-22'

export default function WeekCalendarLimitsExample() {
  const [value, setValue] = React.useState<string | null>(null)
  return (
    <div className="stack">
      <WeekCalendar.Root
        className="calendar"
        aria-label="Sprint"
        aria-describedby="week-calendar-limits-rule"
        locale="en-US"
        today="2026-11-11"
        min="2026-11-08"
        max="2026-12-13"
        isWeekUnavailable={isBooked}
        onValueChange={(next) => setValue(next)}
      >
        <div className="calendar-header">
          <WeekCalendar.PrevButton className="calendar-nav">‹</WeekCalendar.PrevButton>
          <WeekCalendar.Heading className="calendar-heading" />
          <WeekCalendar.NextButton className="calendar-nav">›</WeekCalendar.NextButton>
        </div>
        <WeekCalendar.Grid className="calendar-grid week-calendar-grid">
          {(grid) => (
            <>
              <WeekCalendar.GridHeader>
                <WeekCalendar.GridRow>
                  <WeekCalendar.WeekNumberHeaderCell className="calendar-weekday">
                    Wk
                  </WeekCalendar.WeekNumberHeaderCell>
                  {grid.weekdays.map((weekday) => (
                    <WeekCalendar.HeaderCell
                      key={weekday.index}
                      weekday={weekday.index}
                      className="calendar-weekday"
                    />
                  ))}
                </WeekCalendar.GridRow>
              </WeekCalendar.GridHeader>
              <WeekCalendar.GridBody>
                {grid.weeks.map(({ week, days }) => (
                  <WeekCalendar.Row
                    key={week.start}
                    value={week.start}
                    className="week-calendar-row"
                  >
                    <WeekCalendar.WeekNumber className="week-calendar-number" />
                    {days.map((day) => (
                      <WeekCalendar.Cell
                        key={day.date}
                        date={day.date}
                        className="week-calendar-cell"
                      >
                        <WeekCalendar.Day className="calendar-day" />
                      </WeekCalendar.Cell>
                    ))}
                  </WeekCalendar.Row>
                ))}
              </WeekCalendar.GridBody>
            </>
          )}
        </WeekCalendar.Grid>
      </WeekCalendar.Root>
      <p id="week-calendar-limits-rule" className="muted">
        Weeks from November 8 to December 13. The week of November 22 is booked.
      </p>
      <p className="muted">Sprint: {value ?? 'none'}</p>
    </div>
  )
}

Form

Inside Field and Form: required blocks the submit until a week is chosen, and the first day submits as week=2026-11-15.

WeekCalendarForm.tsx
import * as React from 'react'
import { Field } from '@hamur/react/field'
import { Form } from '@hamur/react/form'
import { WeekCalendar } from '@hamur/react/week-calendar'

export default function WeekCalendarFormExample() {
  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="week" className="field">
        <Field.Label className="calendar-label">Reporting week</Field.Label>
        <WeekCalendar.Root className="calendar" locale="en-US" today="2026-11-11" required>
          <div className="calendar-header">
            <WeekCalendar.PrevButton className="calendar-nav">‹</WeekCalendar.PrevButton>
            <WeekCalendar.Heading className="calendar-heading" />
            <WeekCalendar.NextButton className="calendar-nav">›</WeekCalendar.NextButton>
          </div>
          <WeekCalendar.Grid className="calendar-grid week-calendar-grid">
            {(grid) => (
              <>
                <WeekCalendar.GridHeader>
                  <WeekCalendar.GridRow>
                    <WeekCalendar.WeekNumberHeaderCell className="calendar-weekday">
                      Wk
                    </WeekCalendar.WeekNumberHeaderCell>
                    {grid.weekdays.map((weekday) => (
                      <WeekCalendar.HeaderCell
                        key={weekday.index}
                        weekday={weekday.index}
                        className="calendar-weekday"
                      />
                    ))}
                  </WeekCalendar.GridRow>
                </WeekCalendar.GridHeader>
                <WeekCalendar.GridBody>
                  {grid.weeks.map(({ week, days }) => (
                    <WeekCalendar.Row
                      key={week.start}
                      value={week.start}
                      className="week-calendar-row"
                    >
                      <WeekCalendar.WeekNumber className="week-calendar-number" />
                      {days.map((day) => (
                        <WeekCalendar.Cell
                          key={day.date}
                          date={day.date}
                          className="week-calendar-cell"
                        >
                          <WeekCalendar.Day className="calendar-day" />
                        </WeekCalendar.Cell>
                      ))}
                    </WeekCalendar.Row>
                  ))}
                </WeekCalendar.GridBody>
              </>
            )}
          </WeekCalendar.Grid>
        </WeekCalendar.Root>
        <Field.Error className="error" />
      </Field.Root>
      <button className="button" type="submit">
        Submit
      </button>
      {submitted ? <pre className="output">{submitted}</pre> : null}
    </Form>
  )
}

API reference

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

CellWeekCalendar.Cell

A day of a week, named by its full date. It is not focusable and carries no aria-selected: the row holds focus and the selection. Put a WeekCalendar.Day inside for styling. Renders a <td>.

Cell props
PropTypeDefault
daterequiredThe date of the cell, '2026-11-08'.`${number}-${number}-${number}`—
Cell data attributes
Data attributeDescription
data-selectedPresent when the cell's week is selected.
data-todayPresent on today's date.
data-disabledPresent when the cell's week cannot be focused or chosen.
data-unavailablePresent when the cell's week can be focused but not chosen.
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-valueThe date, YYYY-MM-DD.

DayWeekCalendar.Day

The content of a WeekCalendar.Cell: the day number in the locale's digits, for styling. It carries the same data-* attributes as the cell. Renders a <span>.

Day data attributes
Data attributeDescription
data-selectedPresent when the cell's week is selected.
data-todayPresent on today's date.
data-disabledPresent when the cell's week cannot be focused or chosen.
data-unavailablePresent when the cell's week can be focused but not chosen.
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-valueThe date, YYYY-MM-DD.

GridWeekCalendar.Grid

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

Grid props
PropTypeDefault
childrenThe content, or a function of the month's weeks and weekdays.React.ReactNode | ((grid: WeekCalendarGrid) => React.ReactNode)—

GridBodyWeekCalendar.GridBody

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

GridHeaderWeekCalendar.GridHeader

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

GridRowWeekCalendar.GridRow

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

HeaderCellWeekCalendar.HeaderCell

The name of a weekday above its column. abbr and aria-label are the long name. Renders a <th>.

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

HeadingWeekCalendar.Heading

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

NextButtonWeekCalendar.NextButton

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

NextButton data attributes
Data attributeDescription
data-disabledPresent when there is no later month to show, or the week calendar is disabled.

PrevButtonWeekCalendar.PrevButton

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

PrevButton data attributes
Data attributeDescription
data-disabledPresent when there is no earlier month to show, or the week calendar is disabled.

RootWeekCalendar.Root

Groups all parts of a calendar in which a week is the thing chosen. The value is the week's first day as an ISO 8601 string such as '2026-11-08'; any day is read as the first day of its week. Renders a <div>.

Root props
PropTypeDefault
minThe earliest first day of a week that can be focused or chosen ('2026-11-09').`${number}-${number}-${number}`—
maxThe latest first day of a week that can be focused or chosen.`${number}-${number}-${number}`—
isWeekDisabledWhether a week is disabled: navigation skips it and it cannot be chosen.((week: Week) => boolean)—
isWeekUnavailableWhether a week can be focused but not chosen.((week: Week) => 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 week 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 names, the digits and the week rule.string'en-US'
weekRuleThe week rule: the locale's, or ISO 8601's (weeks start on Monday, and week 1 holds theWeekRuleName'locale'
firstDayOfWeekThe first day of the week, 0 (Sunday) to 6 (Saturday).number—
minimalDaysThe days, 1 to 7, the first week of a year has in that year.number—
visibleMonthThe visible month ('2026-11' or a date).`${number}-${number}-${number}` | `${number}-${number}` | null—
onVisibleMonthChangeEvent handler called when the visible month changes.((month: `${number}-${number}`, eventDetails: WeekCalendarRootVisibleMonthChangeEventDetails) => void)—
fixedWeeksWhether every month shows six weeks.booleanfalse
todayThe date of today ('2026-11-11').`${number}-${number}-${number}`—
timeZoneThe time zone that decides what today is, when today is not given.string—
inputRefA ref to access the hidden input element.React.Ref<HTMLInputElement>—
valueThe selected week's first day, null for none.`${number}-${number}-${number}` | null—
defaultValueThe selected week when initially rendered.`${number}-${number}-${number}` | null—
onValueChangeEvent handler called when the selection changes, with the week's first day, or null when((value: `${number}-${number}-${number}` | null, eventDetails: WeekCalendarRootChangeEventDetails) => void)—
defaultVisibleMonthThe first visible month when initially rendered.`${number}-${number}-${number}` | `${number}-${number}`—
highlightedWeekThe first day of the week that holds focus.`${number}-${number}-${number}` | null—
onHighlightChangeEvent handler called when the focused week changes, with its first day.((start: `${number}-${number}-${number}` | null) => void)—
disallowEmptySelectionWhether the selected week cannot be deselected.booleanfalse
labelsThe texts the calendar adds: the button labels, the week names and the status messages.Partial<WeekCalendarLabels>—
Root data attributes
Data attributeDescription
data-disabledPresent when the week calendar is disabled.
data-readonlyPresent when the week calendar does not allow changing the selection.
data-requiredPresent when the week calendar is required.
data-emptyPresent when no week is selected.
data-validPresent when the week calendar is in a valid state (when wrapped in Field.Root).
data-invalidPresent when the week calendar is in an invalid state (when wrapped in Field.Root).
data-touchedPresent when the week calendar has been touched (when wrapped in Field.Root).
data-dirtyPresent when the week calendar's value has changed (when wrapped in Field.Root).
data-filledPresent when a week is selected (when wrapped in Field.Root).
data-focusedPresent when a row of the week calendar is focused (when wrapped in Field.Root).

RowWeekCalendar.Row

A week, and the thing chosen: the roving tab stop, aria-selected and the week's name are on the row. A click anywhere in it, Enter or Space chooses the week. Renders a <tr>.

Row props
PropTypeDefault
valuerequiredThe week's first day, week.start of one of the grid's weeks.`${number}-${number}-${number}`—
Row data attributes
Data attributeDescription
data-selectedPresent when the week is selected.
data-currentPresent on the week that holds today.
data-disabledPresent when the week cannot be focused or chosen: its first day is before min or after max, or isWeekDisabled.
data-unavailablePresent when the week can be focused but not chosen (isWeekUnavailable).
data-highlightedPresent when the week has focus.
data-valueThe week's first day, YYYY-MM-DD.
data-weekThe week's number in its week-year.
data-week-yearThe week-year of the week.

WeekNumberWeekCalendar.WeekNumber

The number of the row's week, in the locale's digits, as the row's header. Named "Week 46". Renders a <th>.

WeekNumber data attributes
Data attributeDescription
data-selectedPresent when the week is selected.
data-currentPresent on the week that holds today.
data-disabledPresent when the week cannot be focused or chosen.
data-unavailablePresent when the week can be focused but not chosen.

WeekNumberHeaderCellWeekCalendar.WeekNumberHeaderCell

The header of the week number column, named "Week number". Its children are the visible text. Renders a <th>.

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
ArrowUpArrowDownMoves focus a week, without choosing; past the first or last row it shows the next monthRow
ArrowLeftArrowRightAlso moves focus a week: the row is the item, so there is no day to move toRow
ArrowLeftArrowRightSwapped: ArrowLeft moves to the next weekRow
HomeEndMoves focus to the first or last enabled week of the gridRow
PageUpPageDownMoves to the week a month earlier or later and shows that month; with Shift, a yearRow
EnterSpaceChooses the focused week; on the selected week, clears it unless disallowEmptySelectionRow
EnterChooses nothing; the week keeps focusRow
minmaxStay inside the bounds and pass over disabled weeksRow
TabEnters on the week that last had focus, else the selected week, else the current week, else the first oneGrid
EnterSpaceShows the previous or next month; in the month of a bound the button keeps focus and does nothingPrevButton

Accessibility

What the parts set
PartSetsSo that
Rootrole="group", named by aria-label or Field.Label; a polite status region inside itthe calendar is one named control, and a chosen or cleared week is said once
Headingh2, aria-live="polite" and aria-atomica new month is announced when the buttons or the keys change it
Gridrole="grid", aria-labelledby its Heading (aria-label with the month as the fallback), aria-readonly, aria-disabledthe month is read by its name
WeekNumberHeaderCellscope="col", aria-label "Week number"the column is read as "Week number" whatever short text it shows
HeaderCellscope="col", the short name as text and the long name as aria-label and abbrthe weekday column is read as "Monday", not "Mo"
Rowrole="row", roving tabindex, aria-selected, aria-disabled, aria-label such as "Week 46, November 8 to 14, 2026"the focused row carries the week's name and state: "current week", "selected", "unavailable"
WeekNumberrole="rowheader", scope="row", aria-label "Week 46"the number names its row when a screen reader reads by table cell
Cellrole="gridcell", no tab stop and no aria-selected; aria-label the full date; aria-current="date" on todaythe days are read for context while the row is what is chosen
PrevButtonaria-label "Previous month" from labels, aria-disabled at a bound, never disabledthe button keeps focus at min and says it does nothing
NextButtonaria-label "Next month" from labels, aria-disabled at a bound, never disabledthe button keeps focus at max and says it does nothing

Focus order

  1. PrevButton, then NextButton, in the order you render them.
  2. The grid is one tab stop, on a row: the week that last had focus when it is still visible, else the selected week, else the current week, else the first enabled week. Nothing takes focus on mount.
  3. The arrow keys, Home, End, PageUp and PageDown move focus between rows, pass over disabled weeks and change the visible month when they leave it. The day cells never take focus.
  4. Tab leaves the grid.