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 "@lenso/ui";
export default function App() { return <Calendar aria-label="Date" />;}In v3, Calendar uses a compound component pattern with explicit subcomponents:
import { Calendar } from "@lenso/ui";
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 "@lenso/ui";import { today, getLocalTimeZone } from "@internationalized/date";
<Calendar aria-label="Date" defaultValue={today(getLocalTimeZone())}/>Controlled State
import { useState } from "react";import { Calendar } from "@lenso/ui";import { parseDate } from "@internationalized/date";
const [value, setValue] = useState(parseDate("2024-03-07"));
<Calendar aria-label="Date" value={value} onChange={setValue}/>Month and Year Pickers
<Calendar aria-label="Date" showMonthAndYearPickers/>Multiple Months
<Calendar aria-label="Date" visibleMonths={2}/>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)}/>Top and Bottom Content
<Calendar aria-label="Date" topContent={<p>Select a date</p>} bottomContent={ <button onClick={() => setValue(today(getLocalTimeZone()))}> Today </button> }/>Cell Indicators
{/* v2 did not have a built-in cell indicator API */}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