f-ui
Components

Date Range Picker

Multi-month range picker with segmented start/end inputs, optional clear, and popover calendar.

DateRangePicker provides start and end segments and a multi-month range calendar with an optional Clear control. Range value type is DateRange from react-day-picker (from / to). Locale-aware via React Aria.

Installing

pnpm dlx shadcn@latest add https://ui.isaacfei.com/r/date-range-picker.json
npx shadcn@latest add https://ui.isaacfei.com/r/date-range-picker.json
yarn dlx shadcn@latest add https://ui.isaacfei.com/r/date-range-picker.json
bun x shadcn@latest add https://ui.isaacfei.com/r/date-range-picker.json

Or with a namespace: npx shadcn@latest add @f-ui/date-range-picker.

Pulls react-aria-components, @internationalized/date, react-day-picker, lucide-react, plus the shared date-internals registry item and shadcn primitives button, label, popover.

Usage

import { DateRangePicker } from '@/components/f-ui/date-range-picker/date-range-picker';

<DateRangePicker label="Range" value={range} onChange={setRange} clearable />

Examples

Range — start/end segments, multi-month calendar, optional clear.

Date range
mmddyyyy
mmddyyyy

Start and end of the reporting window.

Selected:

"use client";

import { useState } from "react";
import type { DateRange } from "react-day-picker";

import { DateRangePicker } from "@/components/f-ui/date-range-picker/date-range-picker";

function formatRange(range: DateRange | undefined) {
  if (!range?.from && !range?.to) return "—";
  const from = range.from?.toLocaleDateString() ?? "…";
  const to = range.to?.toLocaleDateString() ?? "…";
  return `${from} – ${to}`;
}

export function DateRangePickerDemo() {
  const [range, setRange] = useState<DateRange | undefined>(undefined);

  return (
    <div className="max-w-xl space-y-3">
      <DateRangePicker
        label="Date range"
        value={range}
        onChange={setRange}
        clearable
        placeholder="Start and end of the reporting window."
      />
      <p className="text-muted-foreground text-xs">
        Selected:{" "}
        <span className="text-foreground font-medium tabular-nums">
          {formatRange(range)}
        </span>
      </p>
    </div>
  );
}

Constrained range — restricted to the current year with a custom clear label.

Reporting period
mmddyyyy
mmddyyyy

Constrained to 2026. Weekends selectable.

Selected:

"use client";

import { useState } from "react";
import type { DateRange } from "react-day-picker";

import { DateRangePicker } from "@/components/f-ui/date-range-picker/date-range-picker";

const year = new Date().getFullYear();
const minDate = new Date(year, 0, 1);
const maxDate = new Date(year, 11, 31);

function formatRange(range: DateRange | undefined) {
  if (!range?.from && !range?.to) return "—";
  const from = range.from?.toLocaleDateString() ?? "…";
  const to = range.to?.toLocaleDateString() ?? "…";
  return `${from} – ${to}`;
}

export function DateRangePickerConstrainedDemo() {
  const [range, setRange] = useState<DateRange | undefined>(undefined);

  return (
    <div className="max-w-xl space-y-3">
      <DateRangePicker
        label="Reporting period"
        value={range}
        onChange={setRange}
        minValue={minDate}
        maxValue={maxDate}
        clearable
        clearLabel="Reset"
        placeholder={`Constrained to ${year}. Weekends selectable.`}
      />
      <p className="text-muted-foreground text-xs">
        Selected:{" "}
        <span className="text-foreground font-medium tabular-nums">
          {formatRange(range)}
        </span>
      </p>
    </div>
  );
}

Headless usage

useDateRangePicker returns { state, ariaProps }. Spread ariaProps onto <AriaDateRangePicker> and compose parts like the default or a different shell. The default range field uses end padding for an inline trigger; for a stacked field + full-width button, override padding on the field (e.g. className="!pe-3") and set className="ms-0 w-full …" on the trigger. The demo below uses a distinct card style and stacked controls—open the Code tab there for the full implementation.

Headless · range + custom shell
mmddyyyy
mmddyyyy
"use client";

import { useState } from "react";
import {
  DateRangePicker as AriaDateRangePicker,
  I18nProvider,
  Label,
} from "react-aria-components";
import type { DateRange } from "react-day-picker";

import { RangeCalendar } from "@/components/f-ui/date-internals/calendar-rac";
import { DateRangePickerField } from "@/components/f-ui/date-range-picker/date-range-picker-parts/date-range-picker-field";
import { DateRangePickerPopover } from "@/components/f-ui/date-range-picker/date-range-picker-parts/date-range-picker-popover";
import { DateRangePickerTrigger } from "@/components/f-ui/date-range-picker/date-range-picker-parts/date-range-picker-trigger";
import { useDateRangePicker } from "@/components/f-ui/date-range-picker/use-date-range-picker";

/**
 * Headless demo: same hook + parts as `DateRangePicker`, but **stacked** range field + full-width
 * calendar action and a different visual shell — proves layout and styling are fully in your hands.
 */
export function DateRangePickerHeadlessDemo() {
  const [range, setRange] = useState<DateRange | undefined>(undefined);
  const { ariaProps } = useDateRangePicker({ value: range, onChange: setRange });

  return (
    <I18nProvider locale="en-US">
      <AriaDateRangePicker {...ariaProps} className="flex max-w-lg flex-col gap-3">
        <div className="rounded-2xl border border-cyan-500/25 bg-linear-to-br from-cyan-500/[0.07] via-background to-amber-500/5 p-4 shadow-sm dark:from-cyan-500/12 dark:to-amber-500/10">
          <Label className="text-cyan-800 dark:text-cyan-200 mb-3 block text-[11px] font-semibold tracking-[0.12em] uppercase">
            Headless · range + custom shell
          </Label>
          <div className="flex flex-col gap-2">
            <DateRangePickerField className="rounded-xl bg-background/90 pe-3! shadow-inner ring-1 ring-border" />
            <DateRangePickerTrigger
              aria-label="Open calendar"
              className="ms-0 h-10 w-full min-w-0 shrink-0 justify-center gap-2 rounded-xl border border-cyan-500/35 bg-cyan-500/12 text-sm font-medium text-cyan-900 shadow-none hover:bg-cyan-500/18 dark:text-cyan-100"
            />
          </div>
        </div>
        <DateRangePickerPopover className="rounded-2xl shadow-xl ring-1 ring-cyan-500/20">
          <div className="overflow-auto p-2">
            <RangeCalendar
              visibleDuration={{ months: 1 }}
              className="rounded-xl"
            />
          </div>
        </DateRangePickerPopover>
      </AriaDateRangePicker>
    </I18nProvider>
  );
}

Composition

DateRangePicker (AriaDateRangePicker)
├── Label (optional)
├── div (flex row)
│   ├── DateRangePickerField (Group + start DateInput + "–" + end DateInput)
│   └── DateRangePickerTrigger (Button + CalendarIcon)
├── DateRangePickerPopover (Popover + Dialog)
│   ├── RangeCalendar
│   └── DateRangePickerClear (optional, when `clearable` and value present)
└── helper text (optional, from `placeholder`)

API Reference

Props

PropTypeDefault
valueDateRange (from react-day-picker)
onChange(range?: DateRange) => void
onBlur() => void
localestring (BCP 47)
minValueDate
maxValueDate
isDateUnavailable(date: Date) => boolean
firstDayOfWeek"sun" | ... | "sat"locale default
disabledbooleanfalse
isInvalidboolean
aria-invalidboolean
labelReact.ReactNode
aria-labelstring"Date range" when label is omitted
placeholderstring
classNamestring
classNamesPartial<Record<DateRangePickerSlot, string>>
numberOfMonthsnumber2
clearablebooleanfalse
clearLabelstring"Clear"
calendarTriggerAriaLabelstring"Open calendar"

Slots (classNames)

SlotElement
rootOuter <AriaDateRangePicker> wrapper
field<DateRangePickerField> (start + end segments)
trigger<DateRangePickerTrigger>
popover<DateRangePickerPopover>
calendar<RangeCalendar>
clear<DateRangePickerClear> wrapper (optional)

Hook — useDateRangePicker(options)

Same options shape as useDatePicker, except value is DateRange from react-day-picker and onChange is (range?: DateRange) => void. Returns { state: { hasValue, isInvalid }, ariaProps }.

For accessibility details, see React Aria DateRangePicker.

On this page