apps/docs/content/docs/en/react/migration/(components)/calendar.mdx
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>
);
}
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
v2: Built-in month/year pickers via showMonthAndYearPickers prop
v3: Dedicated compound components: Calendar.YearPickerTrigger, Calendar.YearPickerGrid, Calendar.YearPickerGridBody, Calendar.YearPickerCell
| 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) |
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
<Tabs items={["v2", "v3"]}> <Tab value="v2"> ```tsx import { Calendar } from "@heroui/react"; import { today, getLocalTimeZone } from "@internationalized/date";
<Calendar
aria-label="Date"
defaultValue={today(getLocalTimeZone())}
/>
```
<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>
```
<Tabs items={["v2", "v3"]}> <Tab value="v2"> ```tsx 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}
/>
```
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>
```
<Tabs items={["v2", "v3"]}>
<Tab value="v2">
tsx <Calendar aria-label="Date" showMonthAndYearPickers />
</Tab>
<Tab value="v3">
tsx <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>
</Tab>
</Tabs>
<Tabs items={["v2", "v3"]}>
<Tab value="v2">
tsx <Calendar aria-label="Date" visibleMonths={2} />
</Tab>
<Tab value="v3">
tsx <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>
</Tab>
</Tabs>
<Tabs items={["v2", "v3"]}> <Tab value="v2"> ```tsx import { isWeekend } from "@internationalized/date"; import { useLocale } from "@react-aria/i18n";
const { locale } = useLocale();
<Calendar
aria-label="Date"
isDateUnavailable={(date) => isWeekend(date, locale)}
/>
```
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>
```
<Tabs items={["v2", "v3"]}>
<Tab value="v2">
tsx <Calendar aria-label="Date" topContent={<p>Select a date</p>} bottomContent={ <button onClick={() => setValue(today(getLocalTimeZone()))}> Today </button> } />
</Tab>
<Tab value="v3">
tsx <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>
</Tab>
</Tabs>
<Tabs items={["v2", "v3"]}>
<Tab value="v2">
tsx
</Tab>
<Tab value="v3">
tsx <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>
</Tab>
</Tabs>
classNames Prop<Calendar
classNames={{
base: "custom-base",
prevButton: "custom-prev",
nextButton: "custom-next",
title: "custom-title",
cell: "custom-cell",
cellButton: "custom-cell-button",
}}
/>
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>
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]
showMonthAndYearPickers prop → dedicated Calendar.YearPickerTrigger and Calendar.YearPickerGrid componentsvisibleMonths={n} → visibleDuration={{months: n}} with multiple Calendar.Grid components using offsetCalendarCalendar.CellIndicator and render prop support on Calendar.CellclassNames prop → className on individual compound componentscalendarWidth, weekdayStyle, hideDisabledDates, disableAnimation — use Tailwind CSS or omit