apps/docs/content/docs/en/react/migration/(components)/date-range-picker.mdx
In v2, DateRangePicker was a single self-contained component with props for label, inputs, calendar, and time fields:
import { DateRangePicker } from "@heroui/react";
export default function App() {
return (
<DateRangePicker label="Trip dates" />
);
}
In v3, DateRangePicker uses a composition-first API where you compose DateField and RangeCalendar explicitly:
import { DateField, DateRangePicker, Label, RangeCalendar } from "@heroui/react";
export default function App() {
return (
<DateRangePicker>
<Label>Trip dates</Label>
<DateField.Group>
<DateField.InputContainer>
<DateField.Input slot="start">
{(segment) => <DateField.Segment segment={segment} />}
</DateField.Input>
<DateRangePicker.RangeSeparator />
<DateField.Input slot="end">
{(segment) => <DateField.Segment segment={segment} />}
</DateField.Input>
</DateField.InputContainer>
<DateField.Suffix>
<DateRangePicker.Trigger>
<DateRangePicker.TriggerIndicator />
</DateRangePicker.Trigger>
</DateField.Suffix>
</DateField.Group>
<DateRangePicker.Popover>
<RangeCalendar aria-label="Choose trip dates">
<RangeCalendar.Header>
<RangeCalendar.YearPickerTrigger>
<RangeCalendar.YearPickerTriggerHeading />
<RangeCalendar.YearPickerTriggerIndicator />
</RangeCalendar.YearPickerTrigger>
<RangeCalendar.NavButton slot="previous" />
<RangeCalendar.NavButton slot="next" />
</RangeCalendar.Header>
<RangeCalendar.Grid>
<RangeCalendar.GridHeader>
{(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>}
</RangeCalendar.GridHeader>
<RangeCalendar.GridBody>
{(date) => <RangeCalendar.Cell date={date} />}
</RangeCalendar.GridBody>
</RangeCalendar.Grid>
</RangeCalendar>
</DateRangePicker.Popover>
</DateRangePicker>
);
}
v2: Single DateRangePicker component with all parts (two inputs, separator, calendar, label, popover) handled internally via props
v3: Composition-based API with DateRangePicker, DateField, RangeCalendar, and Label composed together. Two DateField.Input components with slot="start" and slot="end" represent the range inputs.
| v3 Component | Description |
|---|---|
DateRangePicker | Root container and state owner |
DateRangePicker.Trigger | Button that opens the calendar popover |
DateRangePicker.TriggerIndicator | Calendar icon (default) or custom indicator |
DateRangePicker.RangeSeparator | Separator between start and end date inputs (defaults to " - ") |
DateRangePicker.Popover | Popover wrapper for the range calendar |
DateField.Group | Input group wrapper |
DateField.InputContainer | Container for start/end inputs and separator |
DateField.Input slot="start" | Start date segmented input |
DateField.Input slot="end" | End date segmented input |
DateField.Segment | Individual date segment (month, day, year) |
DateField.Suffix | Suffix slot for the trigger button |
RangeCalendar | Full range calendar component (see RangeCalendar migration) |
Label | External label component |
| v2 Prop | v3 Equivalent | Notes |
|---|---|---|
value | value | Same |
defaultValue | defaultValue | Same |
onChange | onChange | Same |
minValue | minValue | Same |
maxValue | maxValue | Same |
isDisabled | isDisabled | Same |
isReadOnly | isReadOnly | Same |
isRequired | isRequired | Same |
isInvalid | isInvalid | Same |
isOpen | isOpen | Same |
defaultOpen | defaultOpen | Same |
onOpenChange | onOpenChange | Same |
granularity | granularity | Same |
hourCycle | hourCycle | Same |
hideTimeZone | hideTimeZone | Same |
shouldForceLeadingZeros | shouldForceLeadingZeros | Same |
allowsNonContiguousRanges | - | Set on RangeCalendar directly |
pageBehavior | - | Set on RangeCalendar directly |
label | - | Use Label component |
description | - | Use Description component |
errorMessage | - | Use FieldError component |
variant | - | Use DateField.Group variant prop or Tailwind CSS |
color | - | Removed (use Tailwind CSS) |
size | - | Removed (use Tailwind CSS) |
radius | - | Removed (use Tailwind CSS) |
labelPlacement | - | Control layout with Tailwind CSS |
selectorIcon | - | Pass children to DateRangePicker.TriggerIndicator |
selectorButtonPlacement | - | Compose trigger position via DateField.Prefix or DateField.Suffix |
visibleMonths | - | Use RangeCalendar visibleDuration prop |
showMonthAndYearPickers | - | Use RangeCalendar.YearPickerTrigger and RangeCalendar.YearPickerGrid |
calendarProps | - | Pass props directly to RangeCalendar |
popoverProps | - | Pass props directly to DateRangePicker.Popover |
selectorButtonProps | - | Pass props directly to DateRangePicker.Trigger |
timeInputProps | - | Compose time input separately |
calendarWidth | - | Removed (use Tailwind CSS) |
validate | - | Handle validation externally |
placeholderValue | placeholderValue | Same |
autoFocus | autoFocus | Same |
disableAnimation | - | Removed |
classNames | - | Use className on individual components |
<Tabs items={["v2", "v3"]}> <Tab value="v2"> ```tsx import { DateRangePicker } from "@heroui/react";
<DateRangePicker label="Trip dates" />
```
<DateRangePicker>
<Label>Trip dates</Label>
<DateField.Group>
<DateField.InputContainer>
<DateField.Input slot="start">
{(segment) => <DateField.Segment segment={segment} />}
</DateField.Input>
<DateRangePicker.RangeSeparator />
<DateField.Input slot="end">
{(segment) => <DateField.Segment segment={segment} />}
</DateField.Input>
</DateField.InputContainer>
<DateField.Suffix>
<DateRangePicker.Trigger>
<DateRangePicker.TriggerIndicator />
</DateRangePicker.Trigger>
</DateField.Suffix>
</DateField.Group>
<DateRangePicker.Popover>
<RangeCalendar aria-label="Choose trip dates">
<RangeCalendar.Header>
<RangeCalendar.YearPickerTrigger>
<RangeCalendar.YearPickerTriggerHeading />
<RangeCalendar.YearPickerTriggerIndicator />
</RangeCalendar.YearPickerTrigger>
<RangeCalendar.NavButton slot="previous" />
<RangeCalendar.NavButton slot="next" />
</RangeCalendar.Header>
<RangeCalendar.Grid>
<RangeCalendar.GridHeader>
{(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>}
</RangeCalendar.GridHeader>
<RangeCalendar.GridBody>
{(date) => <RangeCalendar.Cell date={date} />}
</RangeCalendar.GridBody>
</RangeCalendar.Grid>
</RangeCalendar>
</DateRangePicker.Popover>
</DateRangePicker>
```
<Tabs items={["v2", "v3"]}> <Tab value="v2"> ```tsx import { useState } from "react"; import { DateRangePicker } from "@heroui/react"; import { parseDate } from "@internationalized/date";
const [value, setValue] = useState({
start: parseDate("2024-03-01"),
end: parseDate("2024-03-14"),
});
<DateRangePicker
label="Trip dates"
value={value}
onChange={setValue}
/>
```
const [value, setValue] = useState({
start: parseDate("2024-03-01"),
end: parseDate("2024-03-14"),
});
<DateRangePicker value={value} onChange={setValue}>
<Label>Trip dates</Label>
<DateField.Group>
<DateField.InputContainer>
<DateField.Input slot="start">
{(segment) => <DateField.Segment segment={segment} />}
</DateField.Input>
<DateRangePicker.RangeSeparator />
<DateField.Input slot="end">
{(segment) => <DateField.Segment segment={segment} />}
</DateField.Input>
</DateField.InputContainer>
<DateField.Suffix>
<DateRangePicker.Trigger>
<DateRangePicker.TriggerIndicator />
</DateRangePicker.Trigger>
</DateField.Suffix>
</DateField.Group>
<DateRangePicker.Popover>
<RangeCalendar aria-label="Choose trip dates">
<RangeCalendar.Header>
<RangeCalendar.Heading />
<RangeCalendar.NavButton slot="previous" />
<RangeCalendar.NavButton slot="next" />
</RangeCalendar.Header>
<RangeCalendar.Grid>
<RangeCalendar.GridHeader>
{(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>}
</RangeCalendar.GridHeader>
<RangeCalendar.GridBody>
{(date) => <RangeCalendar.Cell date={date} />}
</RangeCalendar.GridBody>
</RangeCalendar.Grid>
</RangeCalendar>
</DateRangePicker.Popover>
</DateRangePicker>
```
<Tabs items={["v2", "v3"]}>
<Tab value="v2">
tsx <DateRangePicker label="Trip dates" description="Select your travel period" errorMessage="End date must be after start date" isInvalid={isInvalid} />
</Tab>
<Tab value="v3">
```tsx
import { DateField, DateRangePicker, Label, Description, FieldError, RangeCalendar } from "@heroui/react";
<DateRangePicker isInvalid={isInvalid}>
<Label>Trip dates</Label>
<DateField.Group>
<DateField.InputContainer>
<DateField.Input slot="start">
{(segment) => <DateField.Segment segment={segment} />}
</DateField.Input>
<DateRangePicker.RangeSeparator />
<DateField.Input slot="end">
{(segment) => <DateField.Segment segment={segment} />}
</DateField.Input>
</DateField.InputContainer>
<DateField.Suffix>
<DateRangePicker.Trigger>
<DateRangePicker.TriggerIndicator />
</DateRangePicker.Trigger>
</DateField.Suffix>
</DateField.Group>
<Description>Select your travel period</Description>
<FieldError>End date must be after start date</FieldError>
<DateRangePicker.Popover>
<RangeCalendar aria-label="Choose trip dates">
</RangeCalendar>
</DateRangePicker.Popover>
</DateRangePicker>
```
<Tabs items={["v2", "v3"]}> <Tab value="v2"> ```tsx import { Icon } from "@iconify/react";
<DateRangePicker
label="Trip dates"
selectorIcon={<Icon icon="gravity-ui:calendar" />}
/>
```
<DateRangePicker>
<Label>Trip dates</Label>
<DateField.Group>
<DateField.InputContainer>
<DateField.Input slot="start">
{(segment) => <DateField.Segment segment={segment} />}
</DateField.Input>
<DateRangePicker.RangeSeparator />
<DateField.Input slot="end">
{(segment) => <DateField.Segment segment={segment} />}
</DateField.Input>
</DateField.InputContainer>
<DateField.Suffix>
<DateRangePicker.Trigger>
<DateRangePicker.TriggerIndicator>
<Icon icon="gravity-ui:calendar" />
</DateRangePicker.TriggerIndicator>
</DateRangePicker.Trigger>
</DateField.Suffix>
</DateField.Group>
<DateRangePicker.Popover>
<RangeCalendar aria-label="Choose trip dates">
</RangeCalendar>
</DateRangePicker.Popover>
</DateRangePicker>
```
<Tabs items={["v2", "v3"]}>
<Tab value="v2">
tsx <DateRangePicker label="Trip dates" visibleMonths={2} showMonthAndYearPickers />
</Tab>
<Tab value="v3">
tsx <DateRangePicker> <Label>Trip dates</Label> <DateField.Group> <DateField.InputContainer> <DateField.Input slot="start"> {(segment) => <DateField.Segment segment={segment} />} </DateField.Input> <DateRangePicker.RangeSeparator /> <DateField.Input slot="end"> {(segment) => <DateField.Segment segment={segment} />} </DateField.Input> </DateField.InputContainer> <DateField.Suffix> <DateRangePicker.Trigger> <DateRangePicker.TriggerIndicator /> </DateRangePicker.Trigger> </DateField.Suffix> </DateField.Group> <DateRangePicker.Popover> <RangeCalendar aria-label="Choose trip dates" visibleDuration={{months: 2}}> <RangeCalendar.Header> <RangeCalendar.YearPickerTrigger> <RangeCalendar.YearPickerTriggerHeading /> <RangeCalendar.YearPickerTriggerIndicator /> </RangeCalendar.YearPickerTrigger> <RangeCalendar.NavButton slot="previous" /> <RangeCalendar.NavButton slot="next" /> </RangeCalendar.Header> <div className="flex gap-4"> <RangeCalendar.Grid> <RangeCalendar.GridHeader> {(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>} </RangeCalendar.GridHeader> <RangeCalendar.GridBody> {(date) => <RangeCalendar.Cell date={date} />} </RangeCalendar.GridBody> </RangeCalendar.Grid> <RangeCalendar.Grid offset={{months: 1}}> <RangeCalendar.GridHeader> {(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>} </RangeCalendar.GridHeader> <RangeCalendar.GridBody> {(date) => <RangeCalendar.Cell date={date} />} </RangeCalendar.GridBody> </RangeCalendar.Grid> </div> <RangeCalendar.YearPickerGrid> <RangeCalendar.YearPickerGridBody> {(year) => <RangeCalendar.YearPickerCell date={year} />} </RangeCalendar.YearPickerGridBody> </RangeCalendar.YearPickerGrid> </RangeCalendar> </DateRangePicker.Popover> </DateRangePicker>
</Tab>
</Tabs>
classNames Prop<DateRangePicker
classNames={{
base: "custom-base",
label: "custom-label",
selectorButton: "custom-trigger",
selectorIcon: "custom-icon",
separator: "custom-separator",
popoverContent: "custom-popover",
calendar: "custom-calendar",
}}
/>
className Props<DateRangePicker className="custom-base">
<Label className="custom-label">Trip dates</Label>
<DateField.Group>
<DateField.InputContainer>
<DateField.Input slot="start">
{(segment) => <DateField.Segment segment={segment} />}
</DateField.Input>
<DateRangePicker.RangeSeparator className="custom-separator" />
<DateField.Input slot="end">
{(segment) => <DateField.Segment segment={segment} />}
</DateField.Input>
</DateField.InputContainer>
<DateField.Suffix>
<DateRangePicker.Trigger className="custom-trigger">
<DateRangePicker.TriggerIndicator className="custom-icon" />
</DateRangePicker.Trigger>
</DateField.Suffix>
</DateField.Group>
<DateRangePicker.Popover className="custom-popover">
<RangeCalendar aria-label="Choose trip dates" className="custom-calendar">
</RangeCalendar>
</DateRangePicker.Popover>
</DateRangePicker>
The v3 DateRangePicker follows this structure:
DateRangePicker (Root)
├── Label
├── DateField.Group
│ ├── DateField.InputContainer
│ │ ├── DateField.Input slot="start"
│ │ │ └── DateField.Segment (render prop per segment)
│ │ ├── DateRangePicker.RangeSeparator
│ │ └── DateField.Input slot="end"
│ │ └── DateField.Segment (render prop per segment)
│ └── DateField.Suffix
│ └── DateRangePicker.Trigger
│ └── DateRangePicker.TriggerIndicator
├── Description (optional)
├── FieldError (optional)
└── DateRangePicker.Popover
└── RangeCalendar (see RangeCalendar migration guide)
DateRangePicker, DateField, RangeCalendar, and LabelDateField.Input with slot="start" and slot="end"DateRangePicker.RangeSeparator componentlabel prop → Label componentDescription and FieldError componentsRangeCalendar with its own compound components inside DateRangePicker.PopoverselectorIcon prop → pass children to DateRangePicker.TriggerIndicatorshowMonthAndYearPickers prop → use RangeCalendar.YearPickerTrigger and RangeCalendar.YearPickerGridvariant, color, size, radius → use Tailwind CSS or DateField.Group variantclassName on individual compound components