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

Calendar

Migration guide for Calendar from HeroUI v2 to v3

Refer to the v3 Calendar documentation for complete API reference, styling guide, and advanced examples. This guide only focuses on migrating from HeroUI v2.

Structure Changes

In v2, Calendar was a single component configured entirely through props:

import { Calendar } from "@heroui/react";

export default function App() {
  return <Calendar aria-label="Date" />;
}

In v3, Calendar uses a compound component pattern with explicit subcomponents:

import { Calendar } from "@heroui/react";

export default function App() {
  return (
    <Calendar aria-label="Date">
      <Calendar.Header>
        <Calendar.Heading />
        <Calendar.NavButton slot="previous" />
        <Calendar.NavButton slot="next" />
      </Calendar.Header>
      <Calendar.Grid>
        <Calendar.GridHeader>
          {(day) => <Calendar.HeaderCell>{day}</Calendar.HeaderCell>}
        </Calendar.GridHeader>
        <Calendar.GridBody>
          {(date) => <Calendar.Cell date={date} />}
        </Calendar.GridBody>
      </Calendar.Grid>
    </Calendar>
  );
}

Key Changes

1. Component Structure

v2: Single Calendar component with all layout handled internally v3: Compound components: Calendar.Header, Calendar.Heading, Calendar.NavButton, Calendar.Grid, Calendar.GridHeader, Calendar.GridBody, Calendar.HeaderCell, Calendar.Cell, Calendar.CellIndicator

2. Year Picker

v2: Built-in month/year pickers via showMonthAndYearPickers prop v3: Dedicated compound components: Calendar.YearPickerTrigger, Calendar.YearPickerGrid, Calendar.YearPickerGridBody, Calendar.YearPickerCell

3. Prop Changes

v2 Propv3 EquivalentNotes
valuevalueSame
defaultValuedefaultValueSame
onChangeonChangeSame
focusedValuefocusedValueSame
onFocusChangeonFocusChangeSame
minValueminValueSame
maxValuemaxValueSame
isDateUnavailableisDateUnavailableSame
isDisabledisDisabledSame
isReadOnlyisReadOnlySame
isInvalidisInvalidSame
visibleMonthsvisibleDurationChanged to {months: number} object
showMonthAndYearPickers-Use Calendar.YearPickerTrigger and Calendar.YearPickerGrid
onHeaderExpandedChangeonYearPickerOpenChangeRenamed
color-Removed (use Tailwind CSS)
calendarWidth-Removed (use className or Tailwind CSS)
weekdayStyle-Removed
pageBehaviorpageBehaviorSame
firstDayOfWeek-Use I18nProvider locale instead
hideDisabledDates-Removed
disableAnimation-Removed
topContent-Place custom content inside Calendar before Calendar.Grid
bottomContent-Place custom content inside Calendar after Calendar.Grid
classNames-Use className on individual compound components
errorMessage-Removed (handle validation externally)

4. Color Prop Removed

v2: color prop with default, primary, secondary, success, warning, danger v3: No color prop — style cells using Tailwind CSS classes or customize via calendar.css overrides

Migration Examples

Basic Calendar

import { Calendar } from "@heroui/react";
import { today, getLocalTimeZone } from "@internationalized/date";

<Calendar
  aria-label="Date"
  defaultValue={today(getLocalTimeZone())}
/>
import { Calendar } from "@heroui/react";
import { today, getLocalTimeZone } from "@internationalized/date";

<Calendar
  aria-label="Date"
  defaultValue={today(getLocalTimeZone())}
>
  <Calendar.Header>
    <Calendar.Heading />
    <Calendar.NavButton slot="previous" />
    <Calendar.NavButton slot="next" />
  </Calendar.Header>
  <Calendar.Grid>
    <Calendar.GridHeader>
      {(day) => <Calendar.HeaderCell>{day}</Calendar.HeaderCell>}
    </Calendar.GridHeader>
    <Calendar.GridBody>
      {(date) => <Calendar.Cell date={date} />}
    </Calendar.GridBody>
  </Calendar.Grid>
</Calendar>

Controlled State

import { useState } from "react";
import { Calendar } from "@heroui/react";
import { parseDate } from "@internationalized/date";

const [value, setValue] = useState(parseDate("2024-03-07"));

<Calendar
  aria-label="Date"
  value={value}
  onChange={setValue}
/>
import { useState } from "react";
import { Calendar } from "@heroui/react";
import { parseDate } from "@internationalized/date";

const [value, setValue] = useState(parseDate("2024-03-07"));

<Calendar
  aria-label="Date"
  value={value}
  onChange={setValue}
>
  <Calendar.Header>
    <Calendar.Heading />
    <Calendar.NavButton slot="previous" />
    <Calendar.NavButton slot="next" />
  </Calendar.Header>
  <Calendar.Grid>
    <Calendar.GridHeader>
      {(day) => <Calendar.HeaderCell>{day}</Calendar.HeaderCell>}
    </Calendar.GridHeader>
    <Calendar.GridBody>
      {(date) => <Calendar.Cell date={date} />}
    </Calendar.GridBody>
  </Calendar.Grid>
</Calendar>

Month and Year Pickers

<Calendar
  aria-label="Date"
  showMonthAndYearPickers
/>
<Calendar aria-label="Date">
  <Calendar.Header>
    <Calendar.YearPickerTrigger>
      <Calendar.YearPickerTriggerHeading />
      <Calendar.YearPickerTriggerIndicator />
    </Calendar.YearPickerTrigger>
    <Calendar.NavButton slot="previous" />
    <Calendar.NavButton slot="next" />
  </Calendar.Header>
  <Calendar.Grid>
    <Calendar.GridHeader>
      {(day) => <Calendar.HeaderCell>{day}</Calendar.HeaderCell>}
    </Calendar.GridHeader>
    <Calendar.GridBody>
      {(date) => <Calendar.Cell date={date} />}
    </Calendar.GridBody>
  </Calendar.Grid>
  <Calendar.YearPickerGrid>
    <Calendar.YearPickerGridBody>
      {(year) => <Calendar.YearPickerCell date={year} />}
    </Calendar.YearPickerGridBody>
  </Calendar.YearPickerGrid>
</Calendar>

Multiple Months

<Calendar
  aria-label="Date"
  visibleMonths={2}
/>
<Calendar aria-label="Date" visibleDuration={{months: 2}}>
  <Calendar.Header>
    <Calendar.Heading />
    <Calendar.NavButton slot="previous" />
    <Calendar.NavButton slot="next" />
  </Calendar.Header>
  <div className="flex gap-4">
    <Calendar.Grid>
      <Calendar.GridHeader>
        {(day) => <Calendar.HeaderCell>{day}</Calendar.HeaderCell>}
      </Calendar.GridHeader>
      <Calendar.GridBody>
        {(date) => <Calendar.Cell date={date} />}
      </Calendar.GridBody>
    </Calendar.Grid>
    <Calendar.Grid offset={{months: 1}}>
      <Calendar.GridHeader>
        {(day) => <Calendar.HeaderCell>{day}</Calendar.HeaderCell>}
      </Calendar.GridHeader>
      <Calendar.GridBody>
        {(date) => <Calendar.Cell date={date} />}
      </Calendar.GridBody>
    </Calendar.Grid>
  </div>
</Calendar>

Unavailable Dates

import { isWeekend } from "@internationalized/date";
import { useLocale } from "@react-aria/i18n";

const { locale } = useLocale();

<Calendar
  aria-label="Date"
  isDateUnavailable={(date) => isWeekend(date, locale)}
/>
import { isWeekend } from "@internationalized/date";
import { useLocale } from "@react-aria/i18n";

const { locale } = useLocale();

<Calendar
  aria-label="Date"
  isDateUnavailable={(date) => isWeekend(date, locale)}
>
  <Calendar.Header>
    <Calendar.Heading />
    <Calendar.NavButton slot="previous" />
    <Calendar.NavButton slot="next" />
  </Calendar.Header>
  <Calendar.Grid>
    <Calendar.GridHeader>
      {(day) => <Calendar.HeaderCell>{day}</Calendar.HeaderCell>}
    </Calendar.GridHeader>
    <Calendar.GridBody>
      {(date) => <Calendar.Cell date={date} />}
    </Calendar.GridBody>
  </Calendar.Grid>
</Calendar>

Top and Bottom Content

<Calendar
  aria-label="Date"
  topContent={<p>Select a date</p>}
  bottomContent={
    <button onClick={() => setValue(today(getLocalTimeZone()))}>
      Today
    </button>
  }
/>
<Calendar aria-label="Date">
  <p>Select a date</p>
  <Calendar.Header>
    <Calendar.Heading />
    <Calendar.NavButton slot="previous" />
    <Calendar.NavButton slot="next" />
  </Calendar.Header>
  <Calendar.Grid>
    <Calendar.GridHeader>
      {(day) => <Calendar.HeaderCell>{day}</Calendar.HeaderCell>}
    </Calendar.GridHeader>
    <Calendar.GridBody>
      {(date) => <Calendar.Cell date={date} />}
    </Calendar.GridBody>
  </Calendar.Grid>
  <button onClick={() => setValue(today(getLocalTimeZone()))}>
    Today
  </button>
</Calendar>

Cell Indicators

{/* v2 did not have a built-in cell indicator API */}
<Calendar.Grid>
  <Calendar.GridHeader>
    {(day) => <Calendar.HeaderCell>{day}</Calendar.HeaderCell>}
  </Calendar.GridHeader>
  <Calendar.GridBody>
    {(date) => (
      <Calendar.Cell date={date}>
        {({formattedDate}) => (
          <>
            {formattedDate}
            {hasEvent(date) && <Calendar.CellIndicator />}
          </>
        )}
      </Calendar.Cell>
    )}
  </Calendar.GridBody>
</Calendar.Grid>

Styling Changes

v2: classNames Prop

<Calendar
  classNames={{
    base: "custom-base",
    prevButton: "custom-prev",
    nextButton: "custom-next",
    title: "custom-title",
    cell: "custom-cell",
    cellButton: "custom-cell-button",
  }}
/>

v3: Direct className Props

<Calendar className="custom-base">
  <Calendar.Header>
    <Calendar.Heading className="custom-title" />
    <Calendar.NavButton slot="previous" className="custom-prev" />
    <Calendar.NavButton slot="next" className="custom-next" />
  </Calendar.Header>
  <Calendar.Grid>
    <Calendar.GridHeader>
      {(day) => <Calendar.HeaderCell>{day}</Calendar.HeaderCell>}
    </Calendar.GridHeader>
    <Calendar.GridBody>
      {(date) => <Calendar.Cell date={date} className="custom-cell" />}
    </Calendar.GridBody>
  </Calendar.Grid>
</Calendar>

Component Anatomy

The v3 Calendar follows this structure:

Calendar (Root)
  ├── [Custom top content]
  ├── Calendar.Header
  │   ├── Calendar.Heading (or Calendar.YearPickerTrigger)
  │   ├── Calendar.NavButton slot="previous"
  │   └── Calendar.NavButton slot="next"
  ├── Calendar.Grid (one per visible month)
  │   ├── Calendar.GridHeader
  │   │   └── Calendar.HeaderCell (render prop)
  │   └── Calendar.GridBody
  │       └── Calendar.Cell (render prop)
  │           └── Calendar.CellIndicator (optional)
  ├── Calendar.YearPickerGrid (optional)
  │   └── Calendar.YearPickerGridBody
  │       └── Calendar.YearPickerCell
  └── [Custom bottom content]

Summary

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

On this page