Pro--% off in--d : --h : --m : --s
HeroUI

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 Propv3 EquivalentNotes
valuevalueSame
defaultValuedefaultValueSame
onChangeonChangeSame
focusedValuefocusedValueSame
onFocusChangeonFocusChangeSame
minValueminValueSame
maxValuemaxValueSame
isDateUnavailableisDateUnavailableSame
allowsNonContiguousRangesallowsNonContiguousRangesSame
isDisabledisDisabledSame
isReadOnlyisReadOnlySame
isInvalidisInvalidSame
pageBehaviorpageBehaviorSame
visibleMonthsvisibleDurationChanged to {months: number} object
showMonthAndYearPickers-Use RangeCalendar.YearPickerTrigger and RangeCalendar.YearPickerGrid
onHeaderExpandedChangeonYearPickerOpenChangeRenamed
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

  1. Component Structure: Single component → compound components with explicit layout control
  2. Year Picker: showMonthAndYearPickers prop → dedicated RangeCalendar.YearPickerTrigger and RangeCalendar.YearPickerGrid components
  3. Multiple Months: visibleMonths={n}visibleDuration={{months: n}} with multiple RangeCalendar.Grid components using offset
  4. Color Removed: Use Tailwind CSS classes instead
  5. Top/Bottom Content: Props removed → place content directly as children inside RangeCalendar
  6. Cell Customization: New RangeCalendar.CellIndicator and render prop support on RangeCalendar.Cell
  7. Styling: classNames prop → className on individual compound components
  8. Removed Props: calendarWidth, weekdayStyle, showHelper, errorMessage, disableAnimation — use Tailwind CSS or handle externally

On this page