Back to Heroui

Calendar

apps/docs/content/docs/en/react/migration/(components)/calendar.mdx

3.2.312.8 KB
Original Source
<Callout type="info"> Refer to the [v3 Calendar documentation](/docs/react/components/calendar) for complete API reference, styling guide, and advanced examples. This guide only focuses on migrating from HeroUI v2. </Callout>

Structure Changes

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

tsx
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:

tsx
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

<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())}
/>
```
</Tab> <Tab value="v3"> ```tsx 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>
```
</Tab> </Tabs>

Controlled State

<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}
/>
```
</Tab> <Tab value="v3"> ```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}
>
  <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>
```
</Tab> </Tabs>

Month and Year Pickers

<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>

Multiple Months

<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>

Unavailable Dates

<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)}
/>
```
</Tab> <Tab value="v3"> ```tsx 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>
```
</Tab> </Tabs>

Top and Bottom Content

<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>

Cell Indicators

<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>

Styling Changes

v2: classNames Prop

tsx
<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

tsx
<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