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 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 |
isDisabled | isDisabled | Same |
isReadOnly | isReadOnly | Same |
isInvalid | isInvalid | Same |
visibleMonths | visibleDuration | Changed to {months: number} object |
showMonthAndYearPickers | - | Use Calendar.YearPickerTrigger and Calendar.YearPickerGrid |
onHeaderExpandedChange | onYearPickerOpenChange | Renamed |
color | - | Removed (use Tailwind CSS) |
calendarWidth | - | Removed (use className or Tailwind CSS) |
weekdayStyle | - | Removed |
pageBehavior | pageBehavior | Same |
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
- Component Structure: Single component → compound components with explicit layout control
- Year Picker:
showMonthAndYearPickersprop → dedicatedCalendar.YearPickerTriggerandCalendar.YearPickerGridcomponents - Multiple Months:
visibleMonths={n}→visibleDuration={{months: n}}with multipleCalendar.Gridcomponents usingoffset - Color Removed: Use Tailwind CSS classes instead
- Top/Bottom Content: Props removed → place content directly as children inside
Calendar - Cell Customization: New
Calendar.CellIndicatorand render prop support onCalendar.Cell - Styling:
classNamesprop →classNameon individual compound components - Removed Props:
calendarWidth,weekdayStyle,hideDisabledDates,disableAnimation— use Tailwind CSS or omit