# RangeCalendar **Category**: react **URL**: https://heroui.com/en/docs/react/components/range-calendar **Source**: https://raw.githubusercontent.com/heroui-inc/heroui/refs/heads/v3/apps/docs/content/docs/en/react/components/(date-and-time)/range-calendar.mdx > Composable date range picker with month grid, navigation, and year picker support built on React Aria RangeCalendar *** ## Usage ```tsx import { RangeCalendar } from '@heroui/react'; ``` ```tsx "use client"; import {RangeCalendar} from "@heroui/react"; export function Basic() { return ( {(day) => {day}} {(date) => } ); } ``` ## Anatomy ```tsx import {RangeCalendar} from '@heroui/react'; export default () => ( {(day) => {day}} {(date) => } ) ``` ## Examples ### Disabled ```tsx "use client"; import {Description, RangeCalendar} from "@heroui/react"; export function Disabled() { return (
{(day) => {day}} {(date) => } Range calendar is disabled
); } ``` ### Year Picker `RangeCalendar.YearPickerTrigger`, `RangeCalendar.YearPickerGrid`, and their body/cell subcomponents provide an integrated year navigation pattern. ```tsx "use client"; import {RangeCalendar} from "@heroui/react"; export function YearPicker() { return ( {(day) => {day}} {(date) => } {({year}) => } ); } ``` ### Default Value ```tsx "use client"; import {RangeCalendar} from "@heroui/react"; import {parseDate} from "@internationalized/date"; export function DefaultValue() { return ( {(day) => {day}} {(date) => } ); } ``` ### Controlled ```tsx "use client"; import type {DateValue} from "@internationalized/date"; import {Button, ButtonGroup, Description, RangeCalendar} from "@heroui/react"; import { getLocalTimeZone, parseDate, startOfMonth, startOfWeek, today, } from "@internationalized/date"; import {useState} from "react"; import {useLocale} from "react-aria-components"; type DateRange = { start: DateValue; end: DateValue; }; export function Controlled() { const [value, setValue] = useState(null); const [focusedDate, setFocusedDate] = useState(parseDate("2025-12-25")); const {locale} = useLocale(); return (
{(day) => {day}} {(date) => } Selected range: {value ? `${value.start.toString()} -> ${value.end.toString()}` : "(none)"}
); } ``` ### Min and Max Dates ```tsx "use client"; import {Description, RangeCalendar} from "@heroui/react"; import {getLocalTimeZone, today} from "@internationalized/date"; export function MinMaxDates() { const now = today(getLocalTimeZone()); const minDate = now; const maxDate = now.add({months: 3}); return (
{(day) => {day}} {(date) => } Select dates between today and {maxDate.toString()}
); } ``` ### Unavailable Dates Use `isDateUnavailable` to block dates such as weekends, holidays, or booked slots. ```tsx "use client"; import type {DateValue} from "@internationalized/date"; import {Description, RangeCalendar} from "@heroui/react"; import {getLocalTimeZone, today} from "@internationalized/date"; 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; const isDateUnavailable = (date: DateValue) => { return blockedRanges.some(([start, end]) => date.compare(start) >= 0 && date.compare(end) <= 0); }; return (
{(day) => {day}} {(date) => } Some days are unavailable
); } ``` ### 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). ```tsx "use client"; import type {CalendarDate, DateValue} from "@internationalized/date"; import {Description, RangeCalendar} from "@heroui/react"; import {getLocalTimeZone, today} from "@internationalized/date"; export function AnchorUnavailableDates() { const now = today(getLocalTimeZone()); const isDateUnavailable = (date: DateValue, anchorDate: CalendarDate | null) => { return anchorDate != null && Math.abs(date.compare(anchorDate)) > 7; }; return (
{(day) => {day}} {(date) => } After selecting a start date, only dates within 7 days are available
); } ``` ### Weeks in Month Set `weeksInMonth` to a fixed value (for example, `6`) to keep the grid height stable when navigating between months. ```tsx "use client"; import {Description, RangeCalendar} from "@heroui/react"; export function WeeksInMonth() { return (
{(day) => {day}} {(date) => } Always shows 6 weeks per month to avoid layout shift when navigating
); } ``` ### 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. ```tsx "use client"; import {Label, ListBox, RangeCalendar, Select} from "@heroui/react"; import {useState} from "react"; const weekOptions = [ {id: "1", name: "1 week"}, {id: "2", name: "2 weeks"}, {id: "3", name: "3 weeks"}, {id: "4", name: "4 weeks"}, {id: "5", name: "5 weeks"}, {id: "6", name: "6 weeks"}, {id: "8", name: "8 weeks"}, ] as const; export function WeekView() { const [weeks, setWeeks] = useState(1); return (
{(day) => {day}} {(date) => }
); } ``` ### 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. ```tsx "use client"; import {Label, ListBox, RangeCalendar, Select} from "@heroui/react"; import {useState} from "react"; const dayOptions = [ {id: "1", name: "1 day"}, {id: "5", name: "5 days"}, {id: "7", name: "7 days"}, {id: "8", name: "8 days"}, {id: "10", name: "10 days"}, {id: "14", name: "14 days"}, {id: "21", name: "21 days"}, ] as const; export function DayView() { const [days, setDays] = useState(5); return (
{(day) => {day}} {(date) => }
); } ``` ### Allows Non-Contiguous Ranges Enable `allowsNonContiguousRanges` to allow selection across unavailable dates. ```tsx "use client"; import type {DateValue} from "@internationalized/date"; import {Description, RangeCalendar} from "@heroui/react"; import {getLocalTimeZone, today} from "@internationalized/date"; 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; const isDateUnavailable = (date: DateValue) => { return blockedRanges.some(([start, end]) => date.compare(start) >= 0 && date.compare(end) <= 0); }; return (
{(day) => {day}} {(date) => } Non-contiguous ranges are allowed across unavailable dates
); } ``` ### Read Only ```tsx "use client"; import {Description, RangeCalendar} from "@heroui/react"; import {getLocalTimeZone, today} from "@internationalized/date"; export function ReadOnly() { return (
{(day) => {day}} {(date) => } Range calendar is read-only
); } ``` ### Invalid ```tsx "use client"; import type {DateValue} from "@internationalized/date"; import {Description, RangeCalendar} from "@heroui/react"; import {getLocalTimeZone, today} from "@internationalized/date"; import {useState} from "react"; type DateRange = { start: DateValue; end: DateValue; }; export function Invalid() { const now = today(getLocalTimeZone()); const [value, setValue] = useState({ end: now.add({days: 14}), start: now.add({days: 6}), }); const isInvalid = value.end.compare(value.start) > 7; return (
{(day) => {day}} {(date) => } {isInvalid ? (

Maximum stay duration is 1 week

) : ( Select a stay of up to 7 days )}
); } ``` ### Focused Value ```tsx "use client"; import type {DateValue} from "@internationalized/date"; import {Button, Description, RangeCalendar} from "@heroui/react"; import {parseDate} from "@internationalized/date"; import {useState} from "react"; export function FocusedValue() { const [focusedDate, setFocusedDate] = useState(parseDate("2025-06-15")); return (
{(day) => {day}} {(date) => } Focused: {focusedDate.toString()}
); } ``` ### Cell Indicators You can customize `RangeCalendar.Cell` children and use `RangeCalendar.CellIndicator` to display metadata like events. ```tsx "use client"; import {RangeCalendar} from "@heroui/react"; import {getLocalTimeZone, isToday} from "@internationalized/date"; const datesWithEvents = [3, 7, 12, 15, 21, 28]; export function WithIndicators() { return ( {(day) => {day}} {(date) => ( {({formattedDate}) => ( <> {formattedDate} {(isToday(date, getLocalTimeZone()) || datesWithEvents.includes(date.day)) && ( )} )} )} ); } ``` ### Real-World Example ```tsx "use client"; import type {DateValue} from "@internationalized/date"; import {Button, RangeCalendar} from "@heroui/react"; import {getLocalTimeZone, isWeekend, today} from "@internationalized/date"; import {useState} from "react"; import {useLocale} from "react-aria-components"; type DateRange = { start: DateValue; end: DateValue; }; export function BookingCalendar() { const [selectedRange, setSelectedRange] = useState(null); const {locale} = useLocale(); const blockedDates = [5, 6, 12, 13, 14, 20]; const isDateUnavailable = (date: DateValue) => { return isWeekend(date, locale) || blockedDates.includes(date.day); }; return (
{(day) => {day}} {(date) => ( {({formattedDate, isUnavailable}) => ( <> {formattedDate} {!isUnavailable && !isWeekend(date, locale) && blockedDates.includes(date.day) && } )} )}
Blocked dates Weekend/Unavailable
{selectedRange ? ( ) : null}
); } ``` ### Multiple Months Render multiple grids with `visibleDuration` and `offset` for booking and planning experiences. ```tsx "use client"; import {RangeCalendar} from "@heroui/react"; export function MultipleMonths() { return (
{(day) => {day}} {(date) => }
{(day) => {day}} {(date) => }
); } ``` ### 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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/calendar#adding_a_calendar_in_the_locale_string). The example below shows the Indian calendar system: ```tsx "use client"; import {RangeCalendar} from "@heroui/react"; import {I18nProvider} from "react-aria-components"; export function InternationalCalendar() { return ( {(day) => {day}} {(date) => } {({year}) => } ); } ``` **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 ```tsx "use client"; import {RangeCalendar} from "@heroui/react"; import {parseDate} from "@internationalized/date"; const cellClassName = [ "rounded-md", "**:data-[slot=range-calendar-cell-button]:rounded-md", "data-[outside-month=true]:text-muted data-[outside-month=true]:opacity-50", "data-[hovered=true]:not-data-[selected=true]:**:data-[slot=range-calendar-cell-button]:bg-default", "data-[today=true]:**:data-[slot=range-calendar-cell-button]:bg-success-soft", "data-[today=true]:**:data-[slot=range-calendar-cell-button]:text-success-soft-foreground", "data-[today=true]:data-[hovered=true]:not-data-[selected=true]:**:data-[slot=range-calendar-cell-button]:bg-success-soft-hover", "data-[selected=true]:rounded-none data-[selected=true]:bg-success-soft", "data-[outside-month=true]:data-[selected=true]:bg-default/20", "data-[selection-start=true]:rounded-tl-md data-[selection-start=true]:rounded-bl-md", "data-[selection-end=true]:rounded-tr-md data-[selection-end=true]:rounded-br-md", "data-[selection-start=true]:**:data-[slot=range-calendar-cell-button]:bg-success", "data-[selection-start=true]:**:data-[slot=range-calendar-cell-button]:text-success-foreground", "data-[selection-start=true]:data-[pressed=true]:**:data-[slot=range-calendar-cell-button]:bg-success-hover", "data-[selection-end=true]:**:data-[slot=range-calendar-cell-button]:bg-success", "data-[selection-end=true]:**:data-[slot=range-calendar-cell-button]:text-success-foreground", "data-[selection-end=true]:data-[pressed=true]:**:data-[slot=range-calendar-cell-button]:bg-success-hover", ].join(" "); export function CustomStyles() { return ( {(day) => ( {day} )} {(date) => } ); } ``` ### Global CSS ```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](https://getbem.com/) 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](https://react-spectrum.adobe.com/react-aria/RangeCalendar.html). | Prop | Type | Default | Description | |------|------|---------|-------------| | `value` | `RangeValue \| null` | - | Controlled selected range. | | `defaultValue` | `RangeValue \| null` | - | Initial selected range (uncontrolled). | | `onChange` | `(value: RangeValue) => void` | - | Called when selection changes. | | `focusedValue` | `DateValue` | - | Controlled focused date. | | `onFocusChange` | `(value: DateValue) => void` | - | Called when focus moves to another date. | | `minValue` | `DateValue` | Calendar-aware `1900-01-01` | Earliest selectable date. | | `maxValue` | `DateValue` | Calendar-aware `2099-12-31` | Latest selectable date. | | `weeksInMonth` | `number` | - | 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. | | `allowsNonContiguousRanges` | `boolean` | `false` | Allows ranges that span unavailable dates. | | `isDisabled` | `boolean` | `false` | Disables interaction and selection. | | `isReadOnly` | `boolean` | `false` | Keeps content readable but prevents selection changes. | | `isInvalid` | `boolean` | `false` | Marks 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. | | `defaultYearPickerOpen` | `boolean` | `false` | Initial open state of internal year picker. | | `isYearPickerOpen` | `boolean` | - | Controlled year picker open state. | | `onYearPickerOpenChange` | `(isOpen: boolean) => void` | - | Called when year picker open state changes. | ### Composition Parts | Component | Description | |-----------|-------------| | `RangeCalendar.Header` | Header container for navigation and heading. | | `RangeCalendar.Heading` | Formatted heading for the visible range. Supports `offset` (for multi-month layouts) and `format` (month/year/day options). | | `RangeCalendar.NavButton` | Previous/next navigation control (`slot="previous"` or `slot="next"`). | | `RangeCalendar.Grid` | Day grid for one month (`offset` supported for multi-month layouts). | | `RangeCalendar.GridHeader` | Weekday header container. | | `RangeCalendar.GridBody` | Date cell body container. | | `RangeCalendar.HeaderCell` | Weekday label cell. | | `RangeCalendar.Cell` | Individual date cell. | | `RangeCalendar.CellIndicator` | Optional indicator element for custom metadata. | | `RangeCalendar.YearPickerTrigger` | Trigger to toggle year-picker mode. | | `RangeCalendar.YearPickerTriggerHeading` | Localized heading content inside the year-picker trigger. | | `RangeCalendar.YearPickerTriggerIndicator` | Toggle icon inside the year-picker trigger. | | `RangeCalendar.YearPickerGrid` | Overlay year selection grid container. | | `RangeCalendar.YearPickerGridBody` | Body renderer for year grid cells. | | `RangeCalendar.YearPickerCell` | Individual year option cell. | ### Year Picker Parts Year picker subcomponents inherit formatting props from React Aria [`useCalendarHeading`](https://react-aria.adobe.com/useCalendar#usecalendarheading) and [`useCalendarYearPicker`](https://react-aria.adobe.com/useCalendar#usecalendaryearpicker). | Component | Prop | Type | Default | Description | |-----------|------|------|---------|-------------| | `RangeCalendar.YearPickerTriggerHeading` | `format` | `DateFormatterOptions` | - | Customize month/year label (e.g. `{month: 'short'}`). | | `RangeCalendar.YearPickerTriggerHeading` | `offset` | `{months?: number}` | - | Shift the heading relative to the focused date (multi-month layouts). | | `RangeCalendar.YearPickerGrid` | `format` | `DateFormatterOptions` | `{year: 'numeric'}` | Customize year cell labels (era, calendar system, etc.). | | `RangeCalendar.YearPickerGrid` | `visibleYears` | `number` | min–max span or `20` | Number 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: | Prop | Type | Description | |------|------|-------------| | `formattedDate` | `string` | Localized day label for the cell. | | `isSelected` | `boolean` | Whether the date is selected. | | `isSelectionStart` | `boolean` | Whether the date is the start of the selected range. | | `isSelectionEnd` | `boolean` | Whether the date is the end of the selected range. | | `isUnavailable` | `boolean` | Whether the date is unavailable. | | `isDisabled` | `boolean` | Whether the cell is disabled. | | `isOutsideMonth` | `boolean` | Whether the date belongs to adjacent month. | For a complete list of supported calendar systems and their identifiers, see: - [React Aria Calendar Implementations](https://react-aria.adobe.com/internationalized/date/Calendar#implementations) - [React Aria International Calendars](https://react-aria.adobe.com/Calendar#international-calendars) ### Related packages - [`@internationalized/date`](https://react-aria.adobe.com/internationalized/date/) — date types (`CalendarDate`, `CalendarDateTime`, `ZonedDateTime`) and utilities used by all date components - [`I18nProvider`](https://react-aria.adobe.com/I18nProvider) — override locale for a subtree - [`useLocale`](https://react-aria.adobe.com/useLocale) — read the current locale and layout direction ## Related Components ## Related Components - **Calendar**: Interactive month grid for selecting dates - **DateField**: Date input field with labels, descriptions, and validation - **DatePicker**: Composable date picker with date field trigger and calendar popover