Skip to content
Lenso UI

RangeCalendar

Composable date range picker with month grid, navigation, and year picker support built on React Aria RangeCalendar

Usage

import { RangeCalendar } from '@lenso/ui';
"use client";
/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. */import { RangeCalendar } from "@lenso/ui";
export function Basic() {  return (    <RangeCalendar aria-label="Trip dates" firstDayOfWeek="mon">      <RangeCalendar.Header>        <RangeCalendar.Heading />        <RangeCalendar.NavButton slot="previous" />        <RangeCalendar.NavButton slot="next" />      </RangeCalendar.Header>      <RangeCalendar.Grid>        <RangeCalendar.GridHeader>          {(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>}        </RangeCalendar.GridHeader>        <RangeCalendar.GridBody>          {(date) => <RangeCalendar.Cell date={date} />}        </RangeCalendar.GridBody>      </RangeCalendar.Grid>    </RangeCalendar>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Anatomy

import {RangeCalendar} from '@lenso/ui';
export default () => (  <RangeCalendar aria-label="Trip dates">    <RangeCalendar.Header>      <RangeCalendar.Heading />      <RangeCalendar.NavButton slot="previous" />      <RangeCalendar.NavButton slot="next" />    </RangeCalendar.Header>    <RangeCalendar.Grid>      <RangeCalendar.GridHeader>        {(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>}      </RangeCalendar.GridHeader>      <RangeCalendar.GridBody>        {(date) => <RangeCalendar.Cell date={date} />}      </RangeCalendar.GridBody>    </RangeCalendar.Grid>  </RangeCalendar>)

Examples

Disabled

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { RangeCalendar } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { CalendarNote, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";export function Disabled() {  return (    <div {...stylex.props(layout.stack)}>      <RangeCalendar isDisabled aria-label="Trip dates">        <RangeHeader />        <RangeGrid />      </RangeCalendar>      <CalendarNote>Range calendar is disabled</CalendarNote>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Year Picker

RangeCalendar.YearPickerTrigger, RangeCalendar.YearPickerGrid, and their body/cell subcomponents provide an integrated year navigation pattern.

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. */import { RangeCalendar } from "@lenso/ui";import { RangeYearHeader, RangeGrid, RangeYearGrid } from "./demo-parts";export function YearPicker() {  return (    <RangeCalendar aria-label="Trip dates">      <RangeYearHeader />      <RangeGrid />      <RangeYearGrid />    </RangeCalendar>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Default Value

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. */import { RangeCalendar } from "@lenso/ui";import { parseDate } from "@internationalized/date";import { RangeHeader, RangeGrid } from "./demo-parts";export function DefaultValue() {  return (    <RangeCalendar      aria-label="Trip dates"      defaultValue={{ end: parseDate("2025-02-12"), start: parseDate("2025-02-03") }}      firstDayOfWeek="mon"    >      <RangeHeader />      <RangeGrid />    </RangeCalendar>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Controlled

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX and native Base UI Button. */import { Button, ButtonGroup, RangeCalendar } from "@lenso/ui";import {  getLocalTimeZone,  parseDate,  startOfMonth,  startOfWeek,  today,  type DateValue,} from "@internationalized/date";import { useState } from "react";import { useLocale } from "react-aria-components/I18nProvider";import * as stylex from "@stylexjs/stylex";import { CalendarNote, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";export function Controlled() {  const [value, setValue] = useState<{ start: DateValue; end: DateValue } | null>(null);  const [focusedDate, setFocusedDate] = useState<DateValue>(() => parseDate("2025-12-25"));  const { locale } = useLocale();  return (    <div {...stylex.props(layout.stack)}>      <ButtonGroup variant="tertiary">        <Button onClick={() => setFocusedDate(today(getLocalTimeZone()))}>This week</Button>        <Button          onClick={() =>            setFocusedDate(startOfWeek(today(getLocalTimeZone()).add({ weeks: 1 }), locale))          }        >          Next week        </Button>        <Button          onClick={() => setFocusedDate(startOfMonth(today(getLocalTimeZone()).add({ months: 1 })))}        >          Next month        </Button>      </ButtonGroup>      <RangeCalendar        aria-label="Trip dates"        firstDayOfWeek="mon"        focusedValue={focusedDate}        value={value}        onChange={setValue}        onFocusChange={setFocusedDate}      >        <RangeHeader />        <RangeGrid />      </RangeCalendar>      <CalendarNote>        Selected range: {value ? `${value.start.toString()} -> ${value.end.toString()}` : "(none)"}      </CalendarNote>      <div {...stylex.props(layout.controls)}>        <Button          size="sm"          variant="secondary"          onClick={() => {            const start = today(getLocalTimeZone());            setValue({ end: start.add({ days: 6 }), start });            setFocusedDate(start);          }}        >          Set 1 week        </Button>        <Button          size="sm"          variant="secondary"          onClick={() => {            const start = parseDate("2025-12-20");            setValue({ end: parseDate("2025-12-31"), start });            setFocusedDate(start);          }}        >          Set Holidays        </Button>        <Button size="sm" variant="tertiary" onClick={() => setValue(null)}>          Clear        </Button>      </div>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Min and Max Dates

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { RangeCalendar } from "@lenso/ui";import { getLocalTimeZone, today } from "@internationalized/date";import * as stylex from "@stylexjs/stylex";import { CalendarNote, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";export function MinMaxDates() {  const now = today(getLocalTimeZone());  const maxDate = now.add({ months: 3 });  return (    <div {...stylex.props(layout.stack)}>      <RangeCalendar aria-label="Trip dates" maxValue={maxDate} minValue={now}>        <RangeHeader />        <RangeGrid />      </RangeCalendar>      <CalendarNote>Select dates between today and {maxDate.toString()}</CalendarNote>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Unavailable Dates

Use isDateUnavailable to block dates such as weekends, holidays, or booked slots.

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { RangeCalendar } from "@lenso/ui";import { getLocalTimeZone, today } from "@internationalized/date";import * as stylex from "@stylexjs/stylex";import { CalendarNote, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";export function UnavailableDates() {  const now = today(getLocalTimeZone());  const blockedRanges = [    [now.add({ days: 2 }), now.add({ days: 5 })],    [now.add({ days: 12 }), now.add({ days: 13 })],  ] as const;  return (    <div {...stylex.props(layout.stack)}>      <RangeCalendar        aria-label="Trip dates"        defaultValue={{ end: now.add({ days: 9 }), start: now.add({ days: 6 }) }}        firstDayOfWeek="mon"        isDateUnavailable={(date) =>          blockedRanges.some(([start, end]) => date.compare(start) >= 0 && date.compare(end) <= 0)        }      >        <RangeHeader />        <RangeGrid />      </RangeCalendar>      <CalendarNote>Some days are unavailable</CalendarNote>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Anchor-Based Unavailable Dates

When selecting a range, isDateUnavailable receives a second argument, anchorDate, set to the first selected date. Use it to limit which end dates are valid (for example, within 7 days of the start).

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { RangeCalendar } from "@lenso/ui";import { getLocalTimeZone, today } from "@internationalized/date";import * as stylex from "@stylexjs/stylex";import { CalendarNote, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";export function AnchorUnavailableDates() {  return (    <div {...stylex.props(layout.stack)}>      <RangeCalendar        aria-label="Trip dates"        isDateUnavailable={(date, anchorDate) =>          anchorDate != null && Math.abs(date.compare(anchorDate)) > 7        }        minValue={today(getLocalTimeZone())}      >        <RangeHeader />        <RangeGrid />      </RangeCalendar>      <CalendarNote>        After selecting a start date, only dates within 7 days are available      </CalendarNote>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Weeks in Month

Set weeksInMonth to a fixed value (for example, 6) to keep the grid height stable when navigating between months.

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { RangeCalendar } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { CalendarNote, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";export function WeeksInMonth() {  return (    <div {...stylex.props(layout.stack)}>      <RangeCalendar aria-label="Trip dates" weeksInMonth={6}>        <RangeHeader />        <RangeGrid />      </RangeCalendar>      <CalendarNote>        Always shows 6 weeks per month to avoid layout shift when navigating      </CalendarNote>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Week View

Set visibleDuration={{ weeks: n }} to show one or more weeks at a time. Navigation advances by the visible week range. Use pageBehavior="single" to move one week at a time when showing multiple weeks.

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX and native Base UI Select. */import { RangeCalendar } from "@lenso/ui";import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { DurationSelect, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";export function WeekView() {  const [weeks, setWeeks] = useState(1);  return (    <div {...stylex.props(layout.controlsStack)}>      <DurationSelect unit="weeks" value={weeks} onChange={setWeeks} />      <RangeCalendar key={weeks} aria-label="Trip dates" visibleDuration={{ weeks }}>        <RangeHeader />        <RangeGrid />      </RangeCalendar>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Day View

Set visibleDuration={{ days: n }} to show a rolling window of consecutive days. Navigation advances by the visible day range. Use pageBehavior="single" to move one day at a time when showing multiple days.

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX and native Base UI Select. */import { RangeCalendar } from "@lenso/ui";import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { DurationSelect, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";export function DayView() {  const [days, setDays] = useState(5);  return (    <div {...stylex.props(layout.controlsStack)}>      <DurationSelect unit="days" value={days} onChange={setDays} />      <RangeCalendar key={days} aria-label="Trip dates" visibleDuration={{ days }}>        <RangeHeader />        <RangeGrid />      </RangeCalendar>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Allows Non-Contiguous Ranges

Enable allowsNonContiguousRanges to allow selection across unavailable dates.

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { RangeCalendar } from "@lenso/ui";import { getLocalTimeZone, today } from "@internationalized/date";import * as stylex from "@stylexjs/stylex";import { CalendarNote, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";export function AllowsNonContiguousRanges() {  const now = today(getLocalTimeZone());  const blockedRanges = [    [now.add({ days: 2 }), now.add({ days: 5 })],    [now.add({ days: 12 }), now.add({ days: 13 })],  ] as const;  return (    <div {...stylex.props(layout.stack)}>      <RangeCalendar        allowsNonContiguousRanges        aria-label="Trip dates"        defaultValue={{ end: now.add({ days: 9 }), start: now.add({ days: 1 }) }}        firstDayOfWeek="mon"        isDateUnavailable={(date) =>          blockedRanges.some(([start, end]) => date.compare(start) >= 0 && date.compare(end) <= 0)        }      >        <RangeHeader />        <RangeGrid />      </RangeCalendar>      <CalendarNote>Non-contiguous ranges are allowed across unavailable dates</CalendarNote>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Read Only

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { RangeCalendar } from "@lenso/ui";import { getLocalTimeZone, today } from "@internationalized/date";import * as stylex from "@stylexjs/stylex";import { CalendarNote, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";export function ReadOnly() {  const now = today(getLocalTimeZone());  return (    <div {...stylex.props(layout.stack)}>      <RangeCalendar        isReadOnly        aria-label="Trip dates"        defaultValue={{ start: now, end: now.add({ days: 4 }) }}      >        <RangeHeader />        <RangeGrid />      </RangeCalendar>      <CalendarNote>Range calendar is read-only</CalendarNote>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Invalid

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { RangeCalendar } from "@lenso/ui";import { getLocalTimeZone, today, type DateValue } from "@internationalized/date";import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { CalendarNote, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";const styles = stylex.create({ error: { fontSize: 14, color: "var(--danger)" } });export function Invalid() {  const now = today(getLocalTimeZone());  const [value, setValue] = useState<{ start: DateValue; end: DateValue }>({    end: now.add({ days: 14 }),    start: now.add({ days: 6 }),  });  const isInvalid = value.end.compare(value.start) > 7;  return (    <div {...stylex.props(layout.stack)}>      <RangeCalendar        aria-label="Trip dates"        firstDayOfWeek="mon"        isInvalid={isInvalid}        value={value}        onChange={setValue}      >        <RangeHeader />        <RangeGrid />      </RangeCalendar>      {isInvalid ? (        <p {...stylex.props(styles.error)}>Maximum stay duration is 1 week</p>      ) : (        <CalendarNote>Select a stay of up to 7 days</CalendarNote>      )}    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Focused Value

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX and native Base UI Button. */import { Button, RangeCalendar } from "@lenso/ui";import { parseDate, type DateValue } from "@internationalized/date";import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { CalendarNote, layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";export function FocusedValue() {  const [focusedDate, setFocusedDate] = useState<DateValue>(() => parseDate("2025-06-15"));  return (    <div {...stylex.props(layout.stack)}>      <RangeCalendar        aria-label="Trip dates"        firstDayOfWeek="mon"        focusedValue={focusedDate}        onFocusChange={setFocusedDate}      >        <RangeHeader />        <RangeGrid />      </RangeCalendar>      <CalendarNote>Focused: {focusedDate.toString()}</CalendarNote>      <div {...stylex.props(layout.controls)}>        <Button          size="sm"          variant="secondary"          onClick={() => setFocusedDate(parseDate("2025-01-01"))}        >          Go to Jan        </Button>        <Button          size="sm"          variant="secondary"          onClick={() => setFocusedDate(parseDate("2025-06-15"))}        >          Go to Jun        </Button>        <Button          size="sm"          variant="secondary"          onClick={() => setFocusedDate(parseDate("2025-12-25"))}        >          Go to Christmas        </Button>      </div>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Cell Indicators

You can customize RangeCalendar.Cell children and use RangeCalendar.CellIndicator to display metadata like events.

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. */import { RangeCalendar } from "@lenso/ui";import { getLocalTimeZone, isToday } from "@internationalized/date";import { RangeHeader, RangeGrid } from "./demo-parts";const datesWithEvents = [3, 7, 12, 15, 21, 28];export function WithIndicators() {  return (    <RangeCalendar aria-label="Trip dates">      <RangeHeader />      <RangeGrid        cell={(date) => (          <RangeCalendar.Cell date={date}>            {({ formattedDate }) => (              <>                {formattedDate}                {(isToday(date, getLocalTimeZone()) || datesWithEvents.includes(date.day)) && (                  <RangeCalendar.CellIndicator />                )}              </>            )}          </RangeCalendar.Cell>        )}      />    </RangeCalendar>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Real-World Example

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX and native Base UI Button. */import { Button, RangeCalendar } from "@lenso/ui";import { getLocalTimeZone, isWeekend, today, type DateValue } from "@internationalized/date";import { useState } from "react";import { useLocale } from "react-aria-components/I18nProvider";import * as stylex from "@stylexjs/stylex";import { layout } from "../calendar/demo-parts";import { RangeHeader, RangeGrid } from "./demo-parts";const blockedDates = [5, 6, 12, 13, 14, 20];export function BookingCalendar() {  const [selectedRange, setSelectedRange] = useState<{ start: DateValue; end: DateValue } | null>(    null,  );  const { locale } = useLocale();  return (    <div {...stylex.props(layout.stack)}>      <RangeCalendar        aria-label="Booking range"        isDateUnavailable={(date) => isWeekend(date, locale) || blockedDates.includes(date.day)}        minValue={today(getLocalTimeZone())}        value={selectedRange}        onChange={setSelectedRange}      >        <RangeHeader />        <RangeGrid          cell={(date) => (            <RangeCalendar.Cell date={date}>              {({ formattedDate, isUnavailable }) => (                <>                  {formattedDate}                  {!isUnavailable &&                    !isWeekend(date, locale) &&                    blockedDates.includes(date.day) && <RangeCalendar.CellIndicator />}                </>              )}            </RangeCalendar.Cell>          )}        />      </RangeCalendar>      <div {...stylex.props(layout.bookingDetails)}>        <div {...stylex.props(layout.legend)}>          <span {...stylex.props(layout.legendItem)}>            <span {...stylex.props(layout.mutedDot)} /> Blocked dates          </span>          <span {...stylex.props(layout.legendItem)}>            <span {...stylex.props(layout.defaultDot)} /> Weekend/Unavailable          </span>        </div>        {selectedRange ? (          <Button size="sm" variant="primary">            Book {selectedRange.start.toString()} {"->"} {selectedRange.end.toString()}          </Button>        ) : null}      </div>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Multiple Months

Render multiple grids with visibleDuration and offset for booking and planning experiences.

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { RangeCalendar } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  root: { containerType: "normal", width: "100%", maxWidth: "none", overflowX: "auto" },  months: { marginInline: "auto", display: "flex", width: "max-content", gap: 32 },  month: { width: 256 },  heading: { flex: "none" },  spacer: { width: 24, height: 24 },});export function MultipleMonths() {  return (    <RangeCalendar aria-label="Trip dates" xstyle={styles.root} visibleDuration={{ months: 2 }}>      <div {...stylex.props(styles.months)}>        <div {...stylex.props(styles.month)}>          <RangeCalendar.Header>            <RangeCalendar.NavButton slot="previous" />            <RangeCalendar.Heading xstyle={styles.heading} />            <div {...stylex.props(styles.spacer)} />          </RangeCalendar.Header>          <RangeCalendar.Grid>            <RangeCalendar.GridHeader>              {(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>}            </RangeCalendar.GridHeader>            <RangeCalendar.GridBody>              {(date) => <RangeCalendar.Cell date={date} />}            </RangeCalendar.GridBody>          </RangeCalendar.Grid>        </div>        <div {...stylex.props(styles.month)}>          <RangeCalendar.Header>            <div {...stylex.props(styles.spacer)} />            <RangeCalendar.Heading xstyle={styles.heading} offset={{ months: 1 }} />            <RangeCalendar.NavButton slot="next" />          </RangeCalendar.Header>          <RangeCalendar.Grid offset={{ months: 1 }}>            <RangeCalendar.GridHeader>              {(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>}            </RangeCalendar.GridHeader>            <RangeCalendar.GridBody>              {(date) => <RangeCalendar.Cell date={date} />}            </RangeCalendar.GridBody>          </RangeCalendar.Grid>        </div>      </div>    </RangeCalendar>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

International Calendars

By default, RangeCalendar displays dates using the calendar system for the user's locale. You can override this by wrapping your RangeCalendar with I18nProvider and setting the Unicode calendar locale extension.

The example below shows the Indian calendar system:

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. */import { RangeCalendar } from "@lenso/ui";import { I18nProvider } from "react-aria-components/I18nProvider";import { RangeYearHeader, RangeGrid, RangeYearGrid } from "./demo-parts";export function InternationalCalendar() {  return (    <I18nProvider locale="hi-IN-u-ca-indian">      <RangeCalendar aria-label="Trip dates">        <RangeYearHeader />        <RangeGrid />        <RangeYearGrid />      </RangeCalendar>    </I18nProvider>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Note: The onChange event always returns a date in the same calendar system as the value or defaultValue (Gregorian if no value is provided), regardless of the displayed locale.

Customization

Tailwind CSS

"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { RangeCalendar } from "@lenso/ui";import { parseDate } from "@internationalized/date";import * as stylex from "@stylexjs/stylex";
const styles = stylex.create({  root: {    width: 252,    borderRadius: "var(--radius-xl)",    borderWidth: 1,    borderStyle: "solid",    borderColor: "color-mix(in oklab,var(--border) 80%,transparent)",    backgroundColor: "var(--surface)",    padding: 12,    boxShadow:      "0 1px 2px 0 rgb(0 0 0 / .05),0 0 0 1px color-mix(in oklab,var(--success) 10%,transparent)",  },  heading: { color: "var(--foreground)" },  nav: {    borderRadius: "var(--radius-md)",    color: "var(--success)",    backgroundColor: {      default: "transparent",      ":hover": "var(--success-soft)",      ":is([data-hovered])": "var(--success-soft)",    },  },  cell: {    borderRadius: { default: "var(--radius-md)", ":is([data-selected])": 0 },    borderTopLeftRadius: {      default: "var(--radius-md)",      ":is([data-selected])": 0,      ":is([data-selection-start])": "var(--radius-md)",    },    borderBottomLeftRadius: {      default: "var(--radius-md)",      ":is([data-selected])": 0,      ":is([data-selection-start])": "var(--radius-md)",    },    borderTopRightRadius: {      default: "var(--radius-md)",      ":is([data-selected])": 0,      ":is([data-selection-end])": "var(--radius-md)",    },    borderBottomRightRadius: {      default: "var(--radius-md)",      ":is([data-selected])": 0,      ":is([data-selection-end])": "var(--radius-md)",    },    backgroundColor: {      default: null,      ":is([data-selected])": "var(--success-soft)",      ":is([data-selected][data-outside-month])":        "color-mix(in oklab,var(--default) 20%,transparent)",    },  },  button: {    borderRadius: "var(--radius-md)",    backgroundColor: {      default: null,      ":is([data-hovered]:not([data-selected]) > *)": "var(--default)",      ":is([data-today] > *)": "var(--success-soft)",      ":is([data-today][data-hovered]:not([data-selected]) > *)": "var(--success-soft-hover)",      ":is([data-selection-start] > *,[data-selection-end] > *)": "var(--success)",      ":is([data-selection-start][data-pressed] > *,[data-selection-end][data-pressed] > *)":        "var(--success-hover)",    },    color: {      default: null,      ":is([data-today] > *)": "var(--success-soft-foreground)",      ":is([data-selection-start] > *,[data-selection-end] > *)": "var(--success-foreground)",    },  },});export function CustomStyles() {  return (    <RangeCalendar      aria-label="Hotel stay"      xstyle={styles.root}      defaultValue={{ end: parseDate("2025-02-14"), start: parseDate("2025-02-08") }}      firstDayOfWeek="mon"    >      <RangeCalendar.Header>        <RangeCalendar.Heading xstyle={styles.heading} />        <RangeCalendar.NavButton xstyle={styles.nav} slot="previous" />        <RangeCalendar.NavButton xstyle={styles.nav} slot="next" />      </RangeCalendar.Header>      <RangeCalendar.Grid>        <RangeCalendar.GridHeader>          {(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>}        </RangeCalendar.GridHeader>        <RangeCalendar.GridBody>          {(date) => (            <RangeCalendar.Cell date={date} xstyle={styles.cell} buttonXstyle={styles.button} />          )}        </RangeCalendar.GridBody>      </RangeCalendar.Grid>    </RangeCalendar>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Global CSS

@layer components {  .range-calendar {    @apply w-80 rounded-2xl border border-border bg-surface p-3 shadow-sm;  }
  .range-calendar__heading {    @apply text-sm font-semibold text-default;  }
  .range-calendar__cell[data-selected="true"] .range-calendar__cell-button {    @apply bg-accent text-accent-foreground;  }}

Styling Reference

HeroUI follows the BEM methodology to ensure component variants and states are reusable and easy to customize.

CSS Classes

RangeCalendar uses these classes in packages/styles/components/range-calendar.css and packages/styles/components/calendar-year-picker.css:

  • .range-calendar - Root container.
  • .range-calendar__header - Header row containing nav buttons and heading.
  • .range-calendar__heading - Current month label.
  • .range-calendar__nav-button - Previous/next navigation controls.
  • .range-calendar__grid - Main day grid.
  • .range-calendar__grid-header - Weekday header row wrapper.
  • .range-calendar__grid-body - Date rows wrapper.
  • .range-calendar__header-cell - Weekday header cell.
  • .range-calendar__cell - Interactive day cell wrapper.
  • .range-calendar__cell-button - Interactive day button inside each cell.
  • .range-calendar__cell-indicator - Dot indicator inside a day cell.
  • .calendar-year-picker__trigger - Year picker toggle button.
  • .calendar-year-picker__trigger-heading - Heading text inside year picker trigger.
  • .calendar-year-picker__trigger-indicator - Indicator icon inside year picker trigger.
  • .calendar-year-picker__year-grid - Overlay grid of selectable years.
  • .calendar-year-picker__year-cell - Individual year option.

Interactive States

RangeCalendar supports both pseudo-classes and React Aria data attributes:

  • Selected: [data-selected="true"]
  • Selection start: [data-selection-start="true"]
  • Selection end: [data-selection-end="true"]
  • Range middle: [data-selection-in-range="true"]
  • Today: [data-today="true"]
  • Unavailable: [data-unavailable="true"]
  • Outside month: [data-outside-month="true"]
  • Hovered: :hover or [data-hovered="true"]
  • Pressed: :active or [data-pressed="true"]
  • Focus visible: :focus-visible or [data-focus-visible="true"]
  • Disabled: :disabled or [data-disabled="true"]

API Reference

RangeCalendar

RangeCalendar inherits all props from React Aria RangeCalendar.

PropTypeDefaultDescription
valueRangeValue<DateValue> | null-Controlled selected range.
defaultValueRangeValue<DateValue> | null-Initial selected range (uncontrolled).
onChange(value: RangeValue<DateValue>) => void-Called when selection changes.
focusedValueDateValue-Controlled focused date.
onFocusChange(value: DateValue) => void-Called when focus moves to another date.
minValueDateValueCalendar-aware 1900-01-01Earliest selectable date.
maxValueDateValueCalendar-aware 2099-12-31Latest selectable date.
weeksInMonthnumber-The number of weeks in a month. This overrides the default set by the locale.
isDateUnavailable(date: DateValue, anchorDate: CalendarDate | null) => boolean-Marks dates as unavailable. When anchorDate is set, it is the first date the user selected in the current range gesture.
firstDayOfWeek'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'-Overrides the locale default for the first day of the week.
pageBehavior'visible' | 'single''visible'Whether paging advances by the visible duration or one unit at a time.
selectionAlignment'start' | 'center' | 'end''center'Aligns the visible range to the selection on initial render.
allowsNonContiguousRangesbooleanfalseAllows ranges that span unavailable dates.
isDisabledbooleanfalseDisables interaction and selection.
isReadOnlybooleanfalseKeeps content readable but prevents selection changes.
isInvalidbooleanfalseMarks the calendar as invalid for validation UI.
visibleDuration{months?: number; weeks?: number; days?: number}{months: 1}Visible time range. Use { months: n } for month view, { weeks: n } for week view, or { days: n } for day view.
defaultYearPickerOpenbooleanfalseInitial open state of internal year picker.
isYearPickerOpenboolean-Controlled year picker open state.
onYearPickerOpenChange(isOpen: boolean) => void-Called when year picker open state changes.

Composition Parts

ComponentDescription
RangeCalendar.HeaderHeader container for navigation and heading.
RangeCalendar.HeadingFormatted heading for the visible range. Supports offset (for multi-month layouts) and format (month/year/day options).
RangeCalendar.NavButtonPrevious/next navigation control (slot="previous" or slot="next").
RangeCalendar.GridDay grid for one month (offset supported for multi-month layouts).
RangeCalendar.GridHeaderWeekday header container.
RangeCalendar.GridBodyDate cell body container.
RangeCalendar.HeaderCellWeekday label cell.
RangeCalendar.CellIndividual date cell.
RangeCalendar.CellIndicatorOptional indicator element for custom metadata.
RangeCalendar.YearPickerTriggerTrigger to toggle year-picker mode.
RangeCalendar.YearPickerTriggerHeadingLocalized heading content inside the year-picker trigger.
RangeCalendar.YearPickerTriggerIndicatorToggle icon inside the year-picker trigger.
RangeCalendar.YearPickerGridOverlay year selection grid container.
RangeCalendar.YearPickerGridBodyBody renderer for year grid cells.
RangeCalendar.YearPickerCellIndividual year option cell.

Year Picker Parts

Year picker subcomponents inherit formatting props from React Aria useCalendarHeading and useCalendarYearPicker.

ComponentPropTypeDefaultDescription
RangeCalendar.YearPickerTriggerHeadingformatDateFormatterOptions-Customize month/year label (e.g. {month: 'short'}).
RangeCalendar.YearPickerTriggerHeadingoffset{months?: number}-Shift the heading relative to the focused date (multi-month layouts).
RangeCalendar.YearPickerGridformatDateFormatterOptions{year: 'numeric'}Customize year cell labels (era, calendar system, etc.).
RangeCalendar.YearPickerGridvisibleYearsnumbermin–max span or 20Number of years shown in the sliding window. Defaults to the full range between minValue and maxValue when both are set.

RangeCalendar.Cell Render Props

When RangeCalendar.Cell children is a function, React Aria render props are available:

PropTypeDescription
formattedDatestringLocalized day label for the cell.
isSelectedbooleanWhether the date is selected.
isSelectionStartbooleanWhether the date is the start of the selected range.
isSelectionEndbooleanWhether the date is the end of the selected range.
isUnavailablebooleanWhether the date is unavailable.
isDisabledbooleanWhether the cell is disabled.
isOutsideMonthbooleanWhether the date belongs to adjacent month.

For a complete list of supported calendar systems and their identifiers, see:

  • @internationalized/date — date types (CalendarDate, CalendarDateTime, ZonedDateTime) and utilities used by all date components
  • I18nProvider — override locale for a subtree
  • useLocale — read the current locale and layout direction