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 "@lenso/ui";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 "@lenso/ui";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 "@lenso/ui";import { today, getLocalTimeZone } from "@internationalized/date";
<RangeCalendar aria-label="Trip dates" defaultValue={{ start: today(getLocalTimeZone()), end: today(getLocalTimeZone()).add({ weeks: 1 }), }}/>Controlled State
import { useState } from "react";import { RangeCalendar } from "@lenso/ui";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}/>Month and Year Pickers
<RangeCalendar aria-label="Trip dates" showMonthAndYearPickers/>Multiple Months
<RangeCalendar aria-label="Trip dates" visibleMonths={2}/>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/>Top and Bottom Content
<RangeCalendar aria-label="Trip dates" topContent={<p>Select your travel dates</p>} bottomContent={ <button onClick={handleReset}>Reset</button> }/>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