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.jsonnpx shadcn@latest add https://ui.isaacfei.com/r/date-range-picker.jsonyarn dlx shadcn@latest add https://ui.isaacfei.com/r/date-range-picker.jsonbun x shadcn@latest add https://ui.isaacfei.com/r/date-range-picker.jsonOr 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.
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.
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.
"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
| Prop | Type | Default |
|---|---|---|
value | DateRange (from react-day-picker) | — |
onChange | (range?: DateRange) => void | — |
onBlur | () => void | — |
locale | string (BCP 47) | — |
minValue | Date | — |
maxValue | Date | — |
isDateUnavailable | (date: Date) => boolean | — |
firstDayOfWeek | "sun" | ... | "sat" | locale default |
disabled | boolean | false |
isInvalid | boolean | — |
aria-invalid | boolean | — |
label | React.ReactNode | — |
aria-label | string | "Date range" when label is omitted |
placeholder | string | — |
className | string | — |
classNames | Partial<Record<DateRangePickerSlot, string>> | — |
numberOfMonths | number | 2 |
clearable | boolean | false |
clearLabel | string | "Clear" |
calendarTriggerAriaLabel | string | "Open calendar" |
Slots (classNames)
| Slot | Element |
|---|---|
root | Outer <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.