Back to Heroui

DateRangePicker

apps/docs/content/docs/en/react/migration/(components)/date-range-picker.mdx

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

Structure Changes

In v2, DateRangePicker was a single self-contained component with props for label, inputs, calendar, and time fields:

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

tsx
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>
  );
}

Key Changes

1. Component Structure

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.

2. Composition Parts

v3 ComponentDescription
DateRangePickerRoot container and state owner
DateRangePicker.TriggerButton that opens the calendar popover
DateRangePicker.TriggerIndicatorCalendar icon (default) or custom indicator
DateRangePicker.RangeSeparatorSeparator between start and end date inputs (defaults to " - ")
DateRangePicker.PopoverPopover wrapper for the range calendar
DateField.GroupInput group wrapper
DateField.InputContainerContainer for start/end inputs and separator
DateField.Input slot="start"Start date segmented input
DateField.Input slot="end"End date segmented input
DateField.SegmentIndividual date segment (month, day, year)
DateField.SuffixSuffix slot for the trigger button
RangeCalendarFull range calendar component (see RangeCalendar migration)
LabelExternal label component

3. Prop Changes

v2 Propv3 EquivalentNotes
valuevalueSame
defaultValuedefaultValueSame
onChangeonChangeSame
minValueminValueSame
maxValuemaxValueSame
isDisabledisDisabledSame
isReadOnlyisReadOnlySame
isRequiredisRequiredSame
isInvalidisInvalidSame
isOpenisOpenSame
defaultOpendefaultOpenSame
onOpenChangeonOpenChangeSame
granularitygranularitySame
hourCyclehourCycleSame
hideTimeZonehideTimeZoneSame
shouldForceLeadingZerosshouldForceLeadingZerosSame
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
placeholderValueplaceholderValueSame
autoFocusautoFocusSame
disableAnimation-Removed
classNames-Use className on individual components

Migration Examples

Basic DateRangePicker

<Tabs items={["v2", "v3"]}> <Tab value="v2"> ```tsx import { DateRangePicker } from "@heroui/react";

<DateRangePicker label="Trip dates" />
```
</Tab> <Tab value="v3"> ```tsx import { DateField, DateRangePicker, Label, RangeCalendar } from "@heroui/react";
<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>
```
</Tab> </Tabs>

Controlled State

<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}
/>
```
</Tab> <Tab value="v3"> ```tsx import { useState } from "react"; import { DateField, DateRangePicker, Label, RangeCalendar } from "@heroui/react"; import { parseDate } from "@internationalized/date";
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>
```
</Tab> </Tabs>

With Description and Error Message

<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>
```
</Tab> </Tabs>

Custom Selector Icon

<Tabs items={["v2", "v3"]}> <Tab value="v2"> ```tsx import { Icon } from "@iconify/react";

<DateRangePicker
  label="Trip dates"
  selectorIcon={<Icon icon="gravity-ui:calendar" />}
/>
```
</Tab> <Tab value="v3"> ```tsx import { Icon } from "@iconify/react";
<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>
```
</Tab> </Tabs>

With Visible Months and Year Picker

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

Styling Changes

v2: classNames Prop

tsx
<DateRangePicker
  classNames={{
    base: "custom-base",
    label: "custom-label",
    selectorButton: "custom-trigger",
    selectorIcon: "custom-icon",
    separator: "custom-separator",
    popoverContent: "custom-popover",
    calendar: "custom-calendar",
  }}
/>

v3: Direct className Props

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

Component Anatomy

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)

Summary

  1. Component Structure: Single component → composition of DateRangePicker, DateField, RangeCalendar, and Label
  2. Dual Inputs: Built-in start/end inputs → two DateField.Input with slot="start" and slot="end"
  3. Separator: Built-in → DateRangePicker.RangeSeparator component
  4. Label: label prop → Label component
  5. Description/Error: Props → Description and FieldError components
  6. Calendar: Built-in → compose RangeCalendar with its own compound components inside DateRangePicker.Popover
  7. Selector Icon: selectorIcon prop → pass children to DateRangePicker.TriggerIndicator
  8. Year Picker: showMonthAndYearPickers prop → use RangeCalendar.YearPickerTrigger and RangeCalendar.YearPickerGrid
  9. Styling Props Removed: variant, color, size, radius → use Tailwind CSS or DateField.Group variant
  10. ClassNames Removed: Use className on individual compound components