Pro--% off in--d : --h : --m : --s
HeroUI

DateRangePicker

Migration guide for DateRangePicker from HeroUI v2 to v3

Refer to the v3 DateRangePicker documentation for complete API reference, styling guide, and advanced examples. This guide only focuses on migrating from HeroUI v2.

Structure Changes

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

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

import { DateRangePicker } from "@heroui/react";

<DateRangePicker label="Trip dates" />
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>

Controlled State

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

With Description and Error Message

<DateRangePicker
  label="Trip dates"
  description="Select your travel period"
  errorMessage="End date must be after start date"
  isInvalid={isInvalid}
/>
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 compound components */}
    </RangeCalendar>
  </DateRangePicker.Popover>
</DateRangePicker>

Custom Selector Icon

import { Icon } from "@iconify/react";

<DateRangePicker
  label="Trip dates"
  selectorIcon={<Icon icon="gravity-ui:calendar" />}
/>
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 compound components */}
    </RangeCalendar>
  </DateRangePicker.Popover>
</DateRangePicker>

With Visible Months and Year Picker

<DateRangePicker
  label="Trip dates"
  visibleMonths={2}
  showMonthAndYearPickers
/>
<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>

Styling Changes

v2: 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",
  }}
/>

v3: Direct 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 compound components */}
    </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

On this page