Skip to content

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

InputsReact

Date Range Picker

Two DateField inputs, a start and an end, with a calendar popup that picks the whole range.

Rendered by React 19.3.1

Loading React…
import * as React from 'react'
import { Calendar } from '@hamur/react/calendar'
import { DateRangePicker } from '@hamur/react/date-range-picker'
import { Field } from '@hamur/react/field'

export default function DateRangePickerExample() {
  const [stay, setStay] = React.useState<{ start: string; end: string } | null>(null)
  return (
    <div className="stack">
      <Field.Root className="field">
        <Field.Label>Stay</Field.Label>
        <DateRangePicker.Root
          locale="en-US"
          today="2026-11-07"
          onValueChange={setStay}
        >
          <DateRangePicker.Control className="date-field date-picker-control">
            <DateRangePicker.Input
              type="start"
              className="date-field-input date-range-picker-input"
            />
            <DateRangePicker.Separator
              className="date-range-picker-separator"
            >
              –
            </DateRangePicker.Separator>
            <DateRangePicker.Input
              type="end"
              className="date-field-input date-range-picker-input"
            />
            <DateRangePicker.Clear className="date-field-clear">
              ×
            </DateRangePicker.Clear>
            <DateRangePicker.Trigger className="date-picker-trigger">
              <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
                <path d="M2.5 3.5h11v10h-11zM2.5 6.5h11M5.5 1.5v3M10.5 1.5v3" />
              </svg>
            </DateRangePicker.Trigger>
          </DateRangePicker.Control>
          <DateRangePicker.Portal>
            <DateRangePicker.Positioner sideOffset={8}>
              <DateRangePicker.Popup className="popover date-picker-popup">
                <DateRangePicker.Calendar className="calendar">
                  <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>
                </DateRangePicker.Calendar>
                <DateRangePicker.Close className="button date-picker-close">
                  Close
                </DateRangePicker.Close>
              </DateRangePicker.Popup>
            </DateRangePicker.Positioner>
          </DateRangePicker.Portal>
        </DateRangePicker.Root>
      </Field.Root>
      <p className="muted">Stay: {stay ? `${stay.start} to ${stay.end}` : 'none'}</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:

<DateRangePicker.Root>
  <DateRangePicker.Control>
    <DateRangePicker.Input />
    <DateRangePicker.Separator />
    <DateRangePicker.Clear />
    <DateRangePicker.Trigger />
  </DateRangePicker.Control>
  <DateRangePicker.Portal>
    <DateRangePicker.Positioner>
      <DateRangePicker.Popup>
        <DateRangePicker.Calendar />
        <DateRangePicker.Close />
      </DateRangePicker.Popup>
    </DateRangePicker.Positioner>
  </DateRangePicker.Portal>
</DateRangePicker.Root>

// Not in this example: DateRangePicker.Preset, DateRangePicker.Segment

Examples

Presets

Presets for "Next 7 days" and "This month", resolved against today, beside the calendar.

DateRangePickerPresets.tsx
import * as React from 'react'
import type { DateString } from '@hamur/core'
import { Calendar } from '@hamur/react/calendar'
import { DateRangePicker } from '@hamur/react/date-range-picker'
import { Field } from '@hamur/react/field'

// Presets are functions of `today`, which the picker passes in: no clock read in render.
const parse = (date: DateString) => new Date(`${date}T00:00:00Z`)
const iso = (date: Date) => date.toISOString().slice(0, 10) as DateString
const nextSevenDays = (today: DateString) => {
  const end = parse(today)
  end.setUTCDate(end.getUTCDate() + 6)
  return { start: today, end: iso(end) }
}
const thisMonth = (today: DateString) => {
  const date = parse(today)
  const year = date.getUTCFullYear()
  const month = date.getUTCMonth()
  return {
    start: iso(new Date(Date.UTC(year, month, 1))),
    end: iso(new Date(Date.UTC(year, month + 1, 0))),
  }
}

export default function DateRangePickerPresetsExample() {
  const [period, setPeriod] = React.useState<{ start: string; end: string } | null>(null)
  return (
    <div className="stack">
      <Field.Root className="field">
        <Field.Label>Report period</Field.Label>
        <DateRangePicker.Root locale="en-US" today="2026-11-07" onValueChange={setPeriod}>
          <DateRangePicker.Control className="date-field date-picker-control">
            <DateRangePicker.Input
              type="start"
              className="date-field-input date-range-picker-input"
            />
            <DateRangePicker.Separator className="date-range-picker-separator">
              –
            </DateRangePicker.Separator>
            <DateRangePicker.Input
              type="end"
              className="date-field-input date-range-picker-input"
            />
            <DateRangePicker.Trigger className="date-picker-trigger">
              <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
                <path d="M2.5 3.5h11v10h-11zM2.5 6.5h11M5.5 1.5v3M10.5 1.5v3" />
              </svg>
            </DateRangePicker.Trigger>
          </DateRangePicker.Control>
          <DateRangePicker.Portal>
            <DateRangePicker.Positioner sideOffset={8}>
              <DateRangePicker.Popup className="popover date-picker-popup date-picker-with-presets">
                <div role="group" aria-label="Presets" className="date-picker-presets">
                  <DateRangePicker.Preset className="date-picker-preset" value={nextSevenDays}>
                    Next 7 days
                  </DateRangePicker.Preset>
                  <DateRangePicker.Preset className="date-picker-preset" value={thisMonth}>
                    This month
                  </DateRangePicker.Preset>
                </div>
                <DateRangePicker.Calendar className="calendar">
                  <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>
                </DateRangePicker.Calendar>
              </DateRangePicker.Popup>
            </DateRangePicker.Positioner>
          </DateRangePicker.Portal>
        </DateRangePicker.Root>
      </Field.Root>
      <p className="muted">Period: {period ? `${period.start} to ${period.end}` : 'none'}</p>
    </div>
  )
}

Two Months

visibleMonths={2}: two months side by side, with a heading for each.

DateRangePickerTwoMonths.tsx
import * as React from 'react'
import { Calendar } from '@hamur/react/calendar'
import { DateRangePicker } from '@hamur/react/date-range-picker'
import { Field } from '@hamur/react/field'

export default function DateRangePickerTwoMonthsExample() {
  const [trip, setTrip] = React.useState<{ start: string; end: string } | null>(null)
  return (
    <div className="stack">
      <Field.Root className="field">
        <Field.Label>Trip</Field.Label>
        <DateRangePicker.Root
          locale="en-US"
          today="2026-11-07"
          visibleMonths={2}
          onValueChange={setTrip}
        >
          <DateRangePicker.Control className="date-field date-picker-control">
            <DateRangePicker.Input
              type="start"
              className="date-field-input date-range-picker-input"
            />
            <DateRangePicker.Separator className="date-range-picker-separator">
              –
            </DateRangePicker.Separator>
            <DateRangePicker.Input
              type="end"
              className="date-field-input date-range-picker-input"
            />
            <DateRangePicker.Trigger className="date-picker-trigger">
              <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
                <path d="M2.5 3.5h11v10h-11zM2.5 6.5h11M5.5 1.5v3M10.5 1.5v3" />
              </svg>
            </DateRangePicker.Trigger>
          </DateRangePicker.Control>
          <DateRangePicker.Portal>
            <DateRangePicker.Positioner
              sideOffset={8}
              collisionPadding={{ top: 72, right: 8, bottom: 8, left: 8 }}
            >
              <DateRangePicker.Popup className="popover date-picker-popup">
                <DateRangePicker.Calendar className="calendar">
                  <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>
                </DateRangePicker.Calendar>
              </DateRangePicker.Popup>
            </DateRangePicker.Positioner>
          </DateRangePicker.Portal>
        </DateRangePicker.Root>
      </Field.Root>
      <p className="muted">Trip: {trip ? `${trip.start} to ${trip.end}` : 'none'}</p>
    </div>
  )
}

API reference

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

CalendarDateRangePicker.Calendar

The calendar of the popup: Calendar.Root (or RangeCalendar.Root) wired to the picker's value, limits, locale and visible month. Compose it with the Calendar parts (Calendar.PrevButton, Calendar.Heading, Calendar.Grid and the rest). It takes no part in the Field: the control already submits and validates the value. Renders a <div>.

ClearDateRangePicker.Clear

Clears both inputs, including a partly typed one. It is out of the tab order: Backspace clears a segment from the keyboard. Renders a <button>.

CloseDateRangePicker.Close

A button that closes the popover. Renders a <button>.

Close props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue

ControlDateRangePicker.Control

The group that holds the two inputs, the separator and the trigger, and the element the popup is anchored to. It does the Field's work for the range: it registers the value, marks the Field filled and dirty, validates on blur, and counts the popup as inside, so moving focus into the calendar neither touches nor validates the field. Renders a <div>.

InputDateRangePicker.Input

One of the two inputs of the range: a group named "Start date" or "End date" that holds the segments. An end typed before the start is not swapped: the end input is invalid and the Field shows "End date is before start date". Renders a <div>.

Input props
PropTypeDefault
childrenThe segments, or a function of them: ({ segments }) => ....React.ReactNode | ((context: { segments: FieldSegment[]; }) => React.ReactNode)—
typeWhich end of the range the input edits."start" | "end"'start'

PopupDateRangePicker.Popup

The dialog that holds the calendar and the presets. It is named "Choose date" unless you name it. Focus moves to the highlighted day when it opens, and returns to the trigger (or to the segment Alt+Down was pressed on) when it closes. Renders a <div>.

Popup props
PropTypeDefault
initialFocusDetermines the element to focus when the popover is opened.boolean | React.RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | void | HTMLElement | null)—
finalFocusDetermines the element to focus when the popover is closed.boolean | React.RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null)—

PortalDateRangePicker.Portal

A portal element that moves the popup to a different part of the DOM. By default, the portal element is appended to <body>. Renders a <div>.

Portal props
PropTypeDefault
keepMountedWhether to keep the portal mounted in the DOM while the popup is hidden.booleanfalse
containerA parent element to render the portal element into.HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null—

PositionerDateRangePicker.Positioner

Positions the popup against the control (the field and the trigger together). Renders a <div>.

Positioner props
PropTypeDefault
alignHow to align the popup relative to the specified side.Align'start'
anchorAn element to position the popup against.Element | VirtualElement | React.RefObject<Element | null> | (() => Element | VirtualElement | null) | null—
positionMethodDetermines which CSS position property to use."absolute" | "fixed"'absolute'
sideWhich side of the anchor element to align the popup against.Side'bottom'
sideOffsetDistance between the anchor and the popup in pixels.number | OffsetFunction0
alignOffsetAdditional offset along the alignment axis in pixels.number | OffsetFunction0
collisionBoundaryAn element or a rectangle that delimits the area that the popup is confined to.Boundary'clipping-ancestors'
collisionPaddingAdditional space to maintain from the edge of the collision boundary.Padding5
stickyWhether to maintain the popup in the viewport afterbooleanfalse
arrowPaddingMinimum distance to maintain between the arrow and the edges of the popup.number5
disableAnchorTrackingWhether to disable the popup from tracking any layout shift of its positioning anchor.booleanfalse
collisionAvoidanceDetermines how to handle collisions when positioning the popup.CollisionAvoidance—

PresetDateRangePicker.Preset

A button that sets the value to a date or a range, such as "Last 7 days". The value can be a function of today, which the picker reads from the clock after mount (or from today). It closes the popup like a day does. Renders a <button>.

Preset props
PropTypeDefault
valuerequiredThe value the preset sets: a date ('2026-11-07'), a range ({ start, end }), or a function`${number}-${number}-${number}` | DateRange | ((today: `${number}-${number}-${number}`) => `${number}-${number}-${number}` | DateRange)—
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue

RootDateRangePicker.Root

Groups all parts of a pair of fields with a calendar popup that picks a range of dates. It renders no element. Values are { start, end } with ISO 8601 strings; onValueChange is called only with a complete, ordered range, or null.

Root props
PropTypeDefault
openWhether the popup is open.boolean—
defaultOpenWhether the popup is initially open.booleanfalse
onOpenChangeEvent handler called when the popup opens or closes.((open: boolean, eventDetails: DatePickerRootOpenChangeEventDetails) => void)—
onOpenChangeCompleteEvent handler called after any animations complete when the popup opens or closes.((open: boolean) => void)—
actionsRefA ref to imperative actions: unmount and close.React.RefObject<PopoverRootActions | null>—
modalWhether the rest of the page is inert while the popup is open.boolean | "trap-focus"false
closeOnSelectWhether choosing a day (the second end of a range) or a preset closes the popup.booleantrue
minThe earliest date that can be chosen.`${number}-${number}-${number}`—
maxThe latest date that can be chosen.`${number}-${number}-${number}`—
isDateUnavailableWhether a date cannot be chosen but can hold focus.((date: `${number}-${number}-${number}`, anchor: `${number}-${number}-${number}` | null) => boolean)—
isDateDisabledWhether a date is skipped by navigation and cannot be chosen.((date: `${number}-${number}-${number}`) => boolean)—
localeThe BCP 47 locale of the field and the calendar.string'en-US'
firstDayOfWeekThe first day of the week, 0 (Sunday) to 6.number—
visibleMonthThe first visible month.`${number}-${number}-${number}` | `${number}-${number}` | null—
onVisibleMonthChangeEvent handler called when the visible month changes.((month: `${number}-${number}`, eventDetails: DatePickerVisibleMonthChangeEventDetails) => void)—
visibleMonthsHow many months are visible.number1
pageBehaviorWhether Previous and Next move by the visible months or by one."visible" | "single"'visible'
fixedWeeksWhether every month shows six weeks.booleanfalse
todayThe date of today, for the calendar and the presets.`${number}-${number}-${number}`—
timeZoneThe time zone that decides what today is, when today is not given.string—
labelsThe texts the picker, the field and the calendar add (the trigger and popup names, the ClosePartial<DatePickerLabels & DateFieldLabels & CalendarLabels>—
forceLeadingZerosWhether the day, month and hour show a leading zero.boolean—
placeholderValueThe value an empty segment takes on its first Up or Down press.string—
autoAdvanceWhether focus moves to the next segment when the typed number cannot get another digit.booleantrue
segmentRoleThe role of the segments.DateFieldSegmentRole'spinbutton'
onIncompleteChangeEvent handler called when the segments change while no complete value exists.((parts: FieldParts, eventDetails: any) => void)—
disabledWhether the component should ignore user interaction.booleanfalse
readOnlyWhether the user should be unable to change the value.booleanfalse
requiredWhether the user must fill in the field before submitting a form.booleanfalse
invalidWhether the field is invalid.booleanfalse
nameIdentifies the field when a form is submitted.string—
formIdentifies the form that owns the hidden inputs.string—
inputRefA ref to access the first segment.React.Ref<HTMLInputElement>—
childrenReact.ReactNode—
valueThe range, null for none.DateRange | null—
defaultValueThe range when initially rendered.DateRange | null—
onValueChangeEvent handler called when the range changes: a complete, ordered range, or null when a((value: DateRange | null, eventDetails: DatePickerRootChangeEventDetails) => void)—

SegmentDateRangePicker.Segment

One segment of the value. An editable segment (year, month, day, hour, minute, second, millisecond, day period) is a real <input> with the spinbutton role; a literal (/, :) is a <span> hidden from assistive technology., or a <span> for a literal. Renders an <input>.

Segment props
PropTypeDefault
segmentrequiredThe segment to render, one of the segments the Input gives its children.FieldSegment—

SeparatorDateRangePicker.Separator

The mark between the two inputs ("–"). It is hidden from assistive technology. Renders a <span>.

TriggerDateRangePicker.Trigger

A button that opens the popup. It is named "Choose date", or "Change date, Saturday, November 7, 2026" once a date is chosen. It sits inside the control. Renders a <button>.

Trigger props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
idID of the trigger.string—

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
AltArrowDownOn a segment of either input: opens the popup; Escape returns focus to that segmentSegment
EnterSpaceOn a day: the first press sets the start, the second commits the range and closesPopup
EscapeWith a pending start: cancels it and keeps the popup open. Then: closes the popupPopup
BackspaceDeleteIn a segment of a committed range: clears it, and the value becomes nullSegment

Accessibility

What the parts set
PartSetsSo that
Controlrole="group" named by the Field label; the Field's validitythe pair is announced as one control
Inputrole="group" named "Start date" or "End date" (from labels)each end is found and named on its own
Inputthe end input is aria-invalid on its segments when it is before the startthe error is on the element that has focus
Triggeraria-haspopup="dialog", aria-expanded; aria-label "Choose dates" or "Change dates, <start> to <end>"the button says what the range is now
Popuprole="dialog" named "Choose dates"; the calendar announces the pending start in its status regionthe first press is confirmed and the second end is asked for
Separatoraria-hiddenthe dash is not read

Focus order

  1. The segments of the start input, then those of the end input, in the locale's order; Clear is out of the order; then the Trigger.
  2. Opening moves focus to the start of the range (else today) in the calendar.
  3. Closing returns focus to the trigger, or to the segment the popup was opened from.
  4. Clear empties both inputs and focuses the first start segment.