RangeCalendar
Migration guide for RangeCalendar from HeroUI v2 to v3
Refer to the v3 RangeCalendar documentation for complete API reference, styling guide, and advanced examples. This guide only focuses on migrating from HeroUI v2.
Structure Changes
In v2, RangeCalendar was a single component configured entirely through props:
import { RangeCalendar } from "@heroui/react";
import { today, getLocalTimeZone } from "@internationalized/date";
export default function App() {
return (
<RangeCalendar
aria-label="Trip dates"
defaultValue={{
start: today(getLocalTimeZone()),
end: today(getLocalTimeZone()).add({ weeks: 1 }),
}}
/>
);
}In v3, RangeCalendar uses a compound component pattern with explicit subcomponents:
import { RangeCalendar } from "@heroui/react";
import { today, getLocalTimeZone } from "@internationalized/date";
export default function App() {
return (
<RangeCalendar
aria-label="Trip dates"
defaultValue={{
start: today(getLocalTimeZone()),
end: today(getLocalTimeZone()).add({ weeks: 1 }),
}}
>
<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>
);
}Key Changes
1. Component Structure
v2: Single RangeCalendar component with all layout handled internally
v3: Compound components: RangeCalendar.Header, RangeCalendar.Heading, RangeCalendar.NavButton, RangeCalendar.Grid, RangeCalendar.GridHeader, RangeCalendar.GridBody, RangeCalendar.HeaderCell, RangeCalendar.Cell, RangeCalendar.CellIndicator
2. Year Picker
v2: Built-in month/year pickers via showMonthAndYearPickers prop
v3: Dedicated compound components: RangeCalendar.YearPickerTrigger, RangeCalendar.YearPickerGrid, RangeCalendar.YearPickerGridBody, RangeCalendar.YearPickerCell
3. Prop Changes
| v2 Prop | v3 Equivalent | Notes |
|---|---|---|
value | value | Same |
defaultValue | defaultValue | Same |
onChange | onChange | Same |
focusedValue | focusedValue | Same |
onFocusChange | onFocusChange | Same |
minValue | minValue | Same |
maxValue | maxValue | Same |
isDateUnavailable | isDateUnavailable | Same |
allowsNonContiguousRanges | allowsNonContiguousRanges | Same |
isDisabled | isDisabled | Same |
isReadOnly | isReadOnly | Same |
isInvalid | isInvalid | Same |
pageBehavior | pageBehavior | Same |
visibleMonths | visibleDuration | Changed to {months: number} object |
showMonthAndYearPickers | - | Use RangeCalendar.YearPickerTrigger and RangeCalendar.YearPickerGrid |
onHeaderExpandedChange | onYearPickerOpenChange | Renamed |
color | - | Removed (use Tailwind CSS) |
calendarWidth | - | Removed (use className or Tailwind CSS) |
weekdayStyle | - | Removed |
firstDayOfWeek | - | Use I18nProvider locale instead |
topContent | - | Place custom content inside RangeCalendar before the grid |
bottomContent | - | Place custom content inside RangeCalendar after the grid |
errorMessage | - | Removed (handle validation externally) |
showHelper | - | Removed |
disableAnimation | - | Removed |
classNames | - | Use className on individual compound components |
Migration Examples
Basic Range Selection
import { RangeCalendar } from "@heroui/react";
import { today, getLocalTimeZone } from "@internationalized/date";
<RangeCalendar
aria-label="Trip dates"
defaultValue={{
start: today(getLocalTimeZone()),
end: today(getLocalTimeZone()).add({ weeks: 1 }),
}}
/>import { RangeCalendar } from "@heroui/react";
import { today, getLocalTimeZone } from "@internationalized/date";
<RangeCalendar
aria-label="Trip dates"
defaultValue={{
start: today(getLocalTimeZone()),
end: today(getLocalTimeZone()).add({ weeks: 1 }),
}}
>
<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>Controlled State
import { useState } from "react";
import { RangeCalendar } from "@heroui/react";
import { parseDate } from "@internationalized/date";
const [value, setValue] = useState({
start: parseDate("2024-03-01"),
end: parseDate("2024-03-07"),
});
<RangeCalendar
aria-label="Trip dates"
value={value}
onChange={setValue}
/>import { useState } from "react";
import { RangeCalendar } from "@heroui/react";
import { parseDate } from "@internationalized/date";
const [value, setValue] = useState({
start: parseDate("2024-03-01"),
end: parseDate("2024-03-07"),
});
<RangeCalendar
aria-label="Trip dates"
value={value}
onChange={setValue}
>
<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>Month and Year Pickers
<RangeCalendar
aria-label="Trip dates"
showMonthAndYearPickers
/><RangeCalendar aria-label="Trip dates">
<RangeCalendar.Header>
<RangeCalendar.YearPickerTrigger>
<RangeCalendar.YearPickerTriggerHeading />
<RangeCalendar.YearPickerTriggerIndicator />
</RangeCalendar.YearPickerTrigger>
<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.YearPickerGrid>
<RangeCalendar.YearPickerGridBody>
{(year) => <RangeCalendar.YearPickerCell date={year} />}
</RangeCalendar.YearPickerGridBody>
</RangeCalendar.YearPickerGrid>
</RangeCalendar>Multiple Months
<RangeCalendar
aria-label="Trip dates"
visibleMonths={2}
/><RangeCalendar aria-label="Trip dates" visibleDuration={{months: 2}}>
<RangeCalendar.Header>
<RangeCalendar.Heading />
<RangeCalendar.NavButton slot="previous" />
<RangeCalendar.NavButton slot="next" />
</RangeCalendar.Header>
<div className="flex gap-4">
<RangeCalendar.Grid>
<RangeCalendar.GridHeader>
{(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>}
</RangeCalendar.GridHeader>
<RangeCalendar.GridBody>
{(date) => <RangeCalendar.Cell date={date} />}
</RangeCalendar.GridBody>
</RangeCalendar.Grid>
<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>
</RangeCalendar>Unavailable Dates with Non-Contiguous Ranges
import { isWeekend } from "@internationalized/date";
import { useLocale } from "@react-aria/i18n";
const { locale } = useLocale();
<RangeCalendar
aria-label="Trip dates"
isDateUnavailable={(date) => isWeekend(date, locale)}
allowsNonContiguousRanges
/>import { isWeekend } from "@internationalized/date";
import { useLocale } from "@react-aria/i18n";
const { locale } = useLocale();
<RangeCalendar
aria-label="Trip dates"
isDateUnavailable={(date) => isWeekend(date, locale)}
allowsNonContiguousRanges
>
<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>Top and Bottom Content
<RangeCalendar
aria-label="Trip dates"
topContent={<p>Select your travel dates</p>}
bottomContent={
<button onClick={handleReset}>Reset</button>
}
/><RangeCalendar aria-label="Trip dates">
<p>Select your travel dates</p>
<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>
<button onClick={handleReset}>Reset</button>
</RangeCalendar>Styling Changes
v2: classNames Prop
<RangeCalendar
classNames={{
base: "custom-base",
prevButton: "custom-prev",
nextButton: "custom-next",
title: "custom-title",
cell: "custom-cell",
cellButton: "custom-cell-button",
}}
/>v3: Direct className Props
<RangeCalendar className="custom-base">
<RangeCalendar.Header>
<RangeCalendar.Heading className="custom-title" />
<RangeCalendar.NavButton slot="previous" className="custom-prev" />
<RangeCalendar.NavButton slot="next" className="custom-next" />
</RangeCalendar.Header>
<RangeCalendar.Grid>
<RangeCalendar.GridHeader>
{(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>}
</RangeCalendar.GridHeader>
<RangeCalendar.GridBody>
{(date) => <RangeCalendar.Cell date={date} className="custom-cell" />}
</RangeCalendar.GridBody>
</RangeCalendar.Grid>
</RangeCalendar>Component Anatomy
The v3 RangeCalendar follows this structure:
RangeCalendar (Root)
├── [Custom top content]
├── RangeCalendar.Header
│ ├── RangeCalendar.Heading (or RangeCalendar.YearPickerTrigger)
│ ├── RangeCalendar.NavButton slot="previous"
│ └── RangeCalendar.NavButton slot="next"
├── RangeCalendar.Grid (one per visible month)
│ ├── RangeCalendar.GridHeader
│ │ └── RangeCalendar.HeaderCell (render prop)
│ └── RangeCalendar.GridBody
│ └── RangeCalendar.Cell (render prop)
│ └── RangeCalendar.CellIndicator (optional)
├── RangeCalendar.YearPickerGrid (optional)
│ └── RangeCalendar.YearPickerGridBody
│ └── RangeCalendar.YearPickerCell
└── [Custom bottom content]Summary
- Component Structure: Single component → compound components with explicit layout control
- Year Picker:
showMonthAndYearPickersprop → dedicatedRangeCalendar.YearPickerTriggerandRangeCalendar.YearPickerGridcomponents - Multiple Months:
visibleMonths={n}→visibleDuration={{months: n}}with multipleRangeCalendar.Gridcomponents usingoffset - Color Removed: Use Tailwind CSS classes instead
- Top/Bottom Content: Props removed → place content directly as children inside
RangeCalendar - Cell Customization: New
RangeCalendar.CellIndicatorand render prop support onRangeCalendar.Cell - Styling:
classNamesprop →classNameon individual compound components - Removed Props:
calendarWidth,weekdayStyle,showHelper,errorMessage,disableAnimation— use Tailwind CSS or handle externally