Skip to content
Lenso UI

DateRangePicker

Composable date range picker built on React Aria DateRangePicker with DateField and RangeCalendar composition

Usage

import { DateField, DateRangePicker, Label, RangeCalendar } from '@lenso/ui';
"use client";
/** Adapted from the pinned HeroUI v3.2.6 basic.json. Copyright NextUI Inc. Apache-2.0. Local RAC label and StyleX replace source Label and utility classes. */import { DateField, DateRangePicker, RangeCalendar } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";
const styles = stylex.create({ field: { width: 320 } });export function Basic() {  return (    <DateRangePicker xstyle={styles.field} endName="endDate" startName="startDate">      <DateRangePicker.Label>Trip dates</DateRangePicker.Label>      <DateField.Group fullWidth>        <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.Suffix>          <DateRangePicker.Trigger>            <DateRangePicker.TriggerIndicator />          </DateRangePicker.Trigger>        </DateField.Suffix>      </DateField.Group>      <DateRangePicker.Popover>        <RangeCalendar aria-label="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.YearPickerGrid>            <RangeCalendar.YearPickerGridBody>              {({ year }) => <RangeCalendar.YearPickerCell year={year} />}            </RangeCalendar.YearPickerGridBody>          </RangeCalendar.YearPickerGrid>        </RangeCalendar>      </DateRangePicker.Popover>    </DateRangePicker>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Anatomy

DateRangePicker follows a composition-first API. Compose DateField and RangeCalendar explicitly to control structure and styling.

import {DateField, DateRangePicker, Label, RangeCalendar} from '@lenso/ui';
export default () => (  <DateRangePicker>    <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>)

Examples

Disabled

"use client";/** HeroUI v3.2.6 adapted example. Copyright NextUI Inc. Apache-2.0. */export { Disabled } from "./scenarios";

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Controlled

"use client";/** HeroUI v3.2.6 adapted example. Copyright NextUI Inc. Apache-2.0. */export { Controlled } from "./scenarios";

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Validation

"use client";/** HeroUI v3.2.6 adapted example. Copyright NextUI Inc. Apache-2.0. */export { WithValidation } from "./scenarios";

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Format Options

Control how DateRangePicker values are displayed with props such as granularity, hourCycle, hideTimeZone, and shouldForceLeadingZeros.

"use client";/** Adapted from HeroUI v3.2.6 format-options.json. Copyright NextUI Inc. Apache-2.0. */import { DateRangePicker, Separator, TimeField } from "@lenso/ui";import {  DateFormatter,  getLocalTimeZone,  parseDate,  parseZonedDateTime,  type DateValue,} from "@internationalized/date";import { useLocale } from "react-aria-components/I18nProvider";import { useMemo, useState } from "react";import * as stylex from "@stylexjs/stylex";import {  FormatControls,  formatStyles,  type Granularity,  type HourCycle,} from "../date-picker/format-controls";import { PickerCalendar, RangeInput } from "./parts";
export function FormatOptions() {  const [granularity, setGranularity] = useState<Granularity>("minute");  const [hourCycle, setHourCycle] = useState<HourCycle>(12);  const [hideTimeZone, setHideTimeZone] = useState(false);  const [shouldForceLeadingZeros, setShouldForceLeadingZeros] = useState(false);  const { locale } = useLocale();  const dateFormatter = new DateFormatter(locale, {    day: "numeric",    month: "short",    year: "numeric",  });  const defaultValue = useMemo<{ start: DateValue; end: DateValue }>(    () =>      granularity === "day"        ? { start: parseDate("2025-02-03"), end: parseDate("2025-02-10") }        : {            start: parseZonedDateTime(`2026-02-03T08:45:00[${getLocalTimeZone()}]`),            end: parseZonedDateTime(`2026-02-10T18:45:00[${getLocalTimeZone()}]`),          },    [granularity],  );  const timeGranularity = granularity !== "day" ? granularity : undefined;  return (    <div {...stylex.props(formatStyles.rangeStack)}>      <DateRangePicker        key={granularity}        xstyle={formatStyles.rangeField}        defaultValue={defaultValue}        startName="startDate"        endName="endDate"        granularity={granularity}        hourCycle={hourCycle}        hideTimeZone={hideTimeZone}        shouldForceLeadingZeros={shouldForceLeadingZeros}      >        {({ state }) => (          <>            <DateRangePicker.Label>Date range</DateRangePicker.Label>            <RangeInput container />            <DateRangePicker.Popover xstyle={formatStyles.rangePopover}>              <PickerCalendar fill />              {timeGranularity && (                <div {...stylex.props(formatStyles.times)}>                  <div {...stylex.props(formatStyles.timeRow)}>                    <span {...stylex.props(formatStyles.label)}>Start Time</span>                    <TimeField                      aria-label="Start Time"                      granularity={timeGranularity}                      hourCycle={hourCycle}                      hideTimeZone={hideTimeZone}                      name="startTime"                      shouldForceLeadingZeros={shouldForceLeadingZeros}                      value={state.timeRange?.start ?? null}                      onChange={(value) => state.setTime("start", value)}                    >                      <TimeField.Group variant="secondary">                        <TimeField.Input>                          {(segment) => <TimeField.Segment segment={segment} />}                        </TimeField.Input>                      </TimeField.Group>                    </TimeField>                  </div>                  <div {...stylex.props(formatStyles.timeRow)}>                    <span {...stylex.props(formatStyles.label)}>End Time</span>                    <TimeField                      aria-label="End Time"                      granularity={timeGranularity}                      hourCycle={hourCycle}                      hideTimeZone={hideTimeZone}                      name="endTime"                      shouldForceLeadingZeros={shouldForceLeadingZeros}                      value={state.timeRange?.end ?? null}                      onChange={(value) => state.setTime("end", value)}                    >                      <TimeField.Group variant="secondary">                        <TimeField.Input>                          {(segment) => <TimeField.Segment segment={segment} />}                        </TimeField.Input>                      </TimeField.Group>                    </TimeField>                  </div>                </div>              )}              <span {...stylex.props(formatStyles.selected)}>                Selected:{" "}                {state.value?.start && state.value.end                  ? dateFormatter.formatRange(                      state.value.start.toDate(getLocalTimeZone()),                      state.value.end.toDate(getLocalTimeZone()),                    )                  : "No date selected"}              </span>            </DateRangePicker.Popover>          </>        )}      </DateRangePicker>      <Separator xstyle={formatStyles.separator} />      <span {...stylex.props(formatStyles.heading)}>Format Options</span>      <FormatControls        range        {...{          granularity,          setGranularity,          hourCycle,          setHourCycle,          hideTimeZone,          setHideTimeZone,          shouldForceLeadingZeros,          setShouldForceLeadingZeros,        }}      />    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Form Example

"use client";/** HeroUI v3.2.6 adapted example. Copyright NextUI Inc. Apache-2.0. */export { FormExample } from "./scenarios";

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Custom Indicator

DateRangePicker.TriggerIndicator renders the default IconCalendar when no children are provided. Pass children to replace it.

"use client";/** HeroUI v3.2.6 adapted example. Copyright NextUI Inc. Apache-2.0. */export { WithCustomIndicator } from "./scenarios";

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Render Function

"use client";/** Adapted from HeroUI v3.2.6 render-function.json. Copyright NextUI Inc. Apache-2.0. RAC 1.21 owns the native DOM render contract. */import { DateRangePicker } from "@lenso/ui";import { PickerCalendar, RangeInput, styles } from "./parts";
export function RenderFunction() {  return (    <DateRangePicker      xstyle={styles.field}      startName="startDate"      endName="endDate"      render={(props) => <div data-custom="foo" {...props} />}    >      <DateRangePicker.Label>Trip dates</DateRangePicker.Label>      <RangeInput />      <DateRangePicker.Popover>        <PickerCalendar />      </DateRangePicker.Popover>    </DateRangePicker>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

International Calendar

By default, DateRangePicker displays dates using the calendar system for the user's locale. You can override this by wrapping your DateRangePicker with I18nProvider and setting the Unicode calendar locale extension.

The example below shows the Indian calendar system:

"use client";/** HeroUI v3.2.6 adapted example. Copyright NextUI Inc. Apache-2.0. */export { InternationalCalendar } from "./scenarios";

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Note: The onChange event always returns dates in the same calendar system as the value or defaultValue (Gregorian if no value is provided), regardless of the displayed locale.

For a complete list of supported calendar systems and their identifiers, see:

Customization

Tailwind CSS

"use client";/** HeroUI v3.2.6 adapted example. Copyright NextUI Inc. Apache-2.0. */export { CustomStyles } from "./scenarios";

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Global CSS

To customize DateRangePicker base classes, use @layer components.

@layer components {  .date-range-picker {    @apply inline-flex flex-col gap-1;  }
  .date-range-picker__trigger {    @apply inline-flex items-center justify-between;  }
  .date-range-picker__trigger-indicator {    @apply text-muted;  }
  .date-range-picker__range-separator {    @apply px-2 text-default;  }
  .date-range-picker__popover {    @apply min-w-[var(--trigger-width)] p-0;  }}

Styling Reference

HeroUI follows the BEM methodology to ensure component variants and states are reusable and easy to customize.

CSS Classes

DateRangePicker uses these classes in packages/styles/components/date-range-picker.css:

  • .date-range-picker - Root wrapper.
  • .date-range-picker__trigger - Trigger part that opens the popover.
  • .date-range-picker__trigger-indicator - Default/custom indicator slot.
  • .date-range-picker__range-separator - Separator between start and end date inputs.
  • .date-range-picker__popover - Popover content wrapper.

Interactive States

DateRangePicker supports React Aria data attributes and pseudo states:

  • Open: [data-open="true"] on trigger.
  • Disabled: [data-disabled="true"] or [aria-disabled="true"] on trigger.
  • Focus visible: :focus-visible or [data-focus-visible="true"] on trigger.
  • Hover: :hover or [data-hovered="true"] on trigger.

API Reference

DateRangePicker

DateRangePicker inherits all props from React Aria DateRangePicker.

PropTypeDefaultDescription
value{ start: DateValue; end: DateValue } | null-Controlled selected date range value.
defaultValue{ start: DateValue; end: DateValue } | null-Default selected range in uncontrolled mode.
onChange(value: { start: DateValue; end: DateValue } | null) => void-Called when selected range changes.
isOpenboolean-Controlled popover open state.
defaultOpenbooleanfalseInitial popover open state.
onOpenChange(isOpen: boolean) => void-Called when popover open state changes.
isDisabledbooleanfalseDisables range selection and trigger interactions.
isInvalidboolean-Marks the field as invalid for validation state.
minValueDateValue-Minimum selectable date.
maxValueDateValue-Maximum selectable date.
startNamestring-Name used for the start date in HTML form submission.
endNamestring-Name used for the end date in HTML form submission.
childrenReactNode | (values: DateRangePickerRenderProps) => ReactNode-Composed content or render function.
renderDOMRenderFunction<keyof React.JSX.IntrinsicElements, DateRangePickerRenderProps>-Overrides the default DOM element with a custom render function.

Composition Parts

ComponentDescription
DateRangePicker.RootRoot date range picker container and state owner.
DateRangePicker.TriggerTrigger button, usually rendered inside DateField.Suffix.
DateRangePicker.TriggerIndicatorIndicator slot with default calendar icon.
DateRangePicker.RangeSeparatorSeparator part between start and end date inputs.
DateRangePicker.PopoverPopover wrapper for RangeCalendar content.
  • @internationalized/date — date types (CalendarDate, CalendarDateTime, ZonedDateTime) and utilities used by all date components
  • I18nProvider — override locale for a subtree
  • useLocale — read the current locale and layout direction