Marble UI
← Components

Range Calendar

Preview: Range

Month grid for picking a date (or a range with RangeCalendar), locale-aware and keyboard navigable.

Import

import { Calendar, RangeCalendar } from '@marbleui/ui'

This is a style of calendar; see its examples for all variants.

Props

<Calendar>

PropTypeDefaultDescription
months1 | 21Show N months side by side.

Also accepts React Aria props (100).

<RangeCalendar>

PropTypeDefaultDescription
months1 | 21

Also accepts React Aria props (101).

Accessibility

Keyboard interaction, focus management and ARIA semantics come from the underlying primitives: React Aria Button · React Aria Calendar · React Aria CalendarCell · React Aria CalendarGrid · React Aria CalendarGridBody · React Aria CalendarGridHeader · React Aria CalendarHeaderCell · React Aria Heading · React Aria RangeCalendar. Follow the primitive's docs for the full keyboard map.

Usage notes

RangeCalendar picks a start and end date on an always-visible month grid, drawn as one band. It is exported next to Calendar; for a range inside a form field with a popover, use DateRangePicker.

import { today, getLocalTimeZone } from '@internationalized/date'
const t = today(getLocalTimeZone())
<RangeCalendar
  aria-label="Hotel stay"
  months={2}
  minValue={t}
  defaultValue={{ start: t.add({ days: 3 }), end: t.add({ days: 6 }) }}
  onChange={setStay}
/>
  • A range takes two presses: the first sets the start, the band follows the pointer or keyboard focus, the second sets the end. With a mouse or touch, dragging an existing start or end moves that edge.
  • Keyboard: arrow keys move, Enter or Space starts and finishes the range. Screen readers are prompted to start and finish the selection.
  • isDateUnavailable(date, anchor) also receives the first-picked date, so you can cap the length: anchor != null && date.compare(anchor.add({ days: 14 })) > 0. A range can’t cross an unavailable day unless allowsNonContiguousRanges is set.
  • With months={2} the arrows page two months at a time; pageBehavior="single" moves one.
Example source · show
import { getLocalTimeZone, isWeekend, today } from '@internationalized/date'
import { useLocale } from 'react-aria-components'
import { Calendar, RangeCalendar } from './calendar'

function Bookable() {
  const { locale } = useLocale()
  const now = today(getLocalTimeZone())
  return (
    <Calendar
      aria-label="Appointment date"
      minValue={now}
      isDateUnavailable={(d) => isWeekend(d, locale)}
      defaultValue={now.add({ days: isWeekend(now.add({ days: 1 }), locale) ? 3 : 1 })}
    />
  )
}

export const styles = {
  Single: () => <Calendar aria-label="Date" defaultValue={today(getLocalTimeZone())} />,
  Range: () => {
    const now = today(getLocalTimeZone())
    return <RangeCalendar aria-label="Trip" defaultValue={{ start: now.add({ days: 2 }), end: now.add({ days: 9 }) }} />
  },
  'Two months': () => {
    const now = today(getLocalTimeZone())
    return <RangeCalendar aria-label="Stay" months={2} defaultValue={{ start: now.add({ days: 3 }), end: now.add({ days: 6 }) }} />
  },
  Unavailable: Bookable,
}

Source: packages/ui/src/calendar/calendar.tsx

Catalog

Category
date-time
Priority
Core
Primitive
react-aria
React Aria
RangeCalendar