Date Range Picker
Preline-style range picker with integrated segmented start/end field, calendar icon, and optional presets, clear action, time, and confirmable mode.
Date Range Picker combines a native-like segmented start/end field and calendar icon in one bordered shell, with a VCP-powered Calendar popover. It supports one or two visible months, optional confirm/apply flow, optional time controls, and preset shortcuts using plain { from?: Date; to?: Date } values.
Interactions
| Event | Behavior |
|---|---|
| Calendar trigger click | Toggle popover |
| Focus any range segment | Open popover |
| Edit segments while popover is open | Popover stays open; calendar syncs on commit |
| Pick start date (default) | Commit { from }, keep popover open |
| Pick complete range (no time) | Commit { from, to }, close popover |
Pick complete range (withTime) | Commit draft, keep popover open; set start/end time separately |
| Preset click | Commit preset range, close popover |
Clear (when showClear) | Clear value, close popover |
Cancel (confirmable) | Revert draft, close popover |
Apply (confirmable) | Commit draft, close popover |
| Outside click / Escape | Close popover (withTime commits draft on close) |
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.
Usage
import {
DateRangePicker,
buildDefaultDateRangePresets,
} from "@/components/f-ui/date-range-picker/date-range-picker";
const presets = buildDefaultDateRangePresets((key) => key);
<DateRangePicker
label="Date range"
value={range}
onChange={setRange}
visibleMonths={2}
presets={presets}
showClear
/>;Examples
Two-Month Range
Default variant — dual-month calendar with optional presets and clear action.
"use client";
import { useState } from "react";
import type { DateRangeValue } from "@/components/f-ui/date-internals/date-value";
import {
buildDefaultDateRangePresets,
DateRangePicker,
} from "@/components/f-ui/date-range-picker/date-range-picker";
import { useDateRangePickerI18n } from "@/components/f-ui/date-range-picker/hooks/use-date-range-picker-i18n";
export function DateRangePickerDemo() {
const [range, setRange] = useState<DateRangeValue | undefined>(undefined);
const { t } = useDateRangePickerI18n();
const presets = buildDefaultDateRangePresets(t);
return (
<div className="max-w-xl w-full">
<DateRangePicker
label="Date range"
value={range}
onChange={setRange}
showClear
presets={presets}
/>
</div>
);
}Single-Month Range
Set visibleMonths={1} for a compact panel — the popover width follows the single calendar grid (no extra gutter on the right).
"use client";
import { useState } from "react";
import type { DateRangeValue } from "@/components/f-ui/date-internals/date-value";
import { DateRangePicker } from "@/components/f-ui/date-range-picker/date-range-picker";
export function DateRangePickerSingleMonthDemo() {
const [range, setRange] = useState<DateRangeValue | undefined>(undefined);
return (
<div className="max-w-xl w-full">
<DateRangePicker
label="Date range"
value={range}
onChange={setRange}
visibleMonths={1}
showClear
/>
</div>
);
}Confirmable Range
Draft values in the popover apply only after footer Apply; Cancel reverts and closes.
"use client";
import { useState } from "react";
import type { DateRangeValue } from "@/components/f-ui/date-internals/date-value";
import { DateRangePicker } from "@/components/f-ui/date-range-picker/date-range-picker";
export function DateRangePickerConfirmDemo() {
const [range, setRange] = useState<DateRangeValue | undefined>(undefined);
return (
<div className="max-w-xl w-full">
<DateRangePicker
label="Date range"
value={range}
onChange={setRange}
confirmable
visibleMonths={2}
/>
</div>
);
}Range + Time
withTime adds Start time and End time controls below the calendar (hour/minute fields using the same border, ring, and segment styling as the date field — no native browser time picker). Times are independent per endpoint. The segmented field also exposes hour/minute segments. The popover stays open after day selection until you finish times or dismiss; closing commits the draft range.
"use client";
import { useState } from "react";
import type { DateRangeValue } from "@/components/f-ui/date-internals/date-value";
import { DateRangePicker } from "@/components/f-ui/date-range-picker/date-range-picker";
export function DateRangePickerWithTimeDemo() {
const [range, setRange] = useState<DateRangeValue | undefined>(undefined);
return (
<div className="max-w-xl w-full">
<DateRangePicker
label="Date range and time"
value={range}
onChange={setRange}
withTime
visibleMonths={2}
showClear
/>
</div>
);
}Constrained Range
minValue and maxValue bound both the segmented inputs and calendar selection.
"use client";
import { useState } from "react";
import type { DateRangeValue } from "@/components/f-ui/date-internals/date-value";
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);
export function DateRangePickerConstrainedDemo() {
const [range, setRange] = useState<DateRangeValue | undefined>(undefined);
return (
<div className="max-w-xl w-full">
<DateRangePicker
label="Reporting period"
value={range}
onChange={setRange}
minValue={minDate}
maxValue={maxDate}
showClear
visibleMonths={2}
showToday
/>
</div>
);
}Split Layout (Start / End Pickers)
Set layout="split" for two separate DatePicker fields — one for start, one for end. Best for narrow filter panels, dialogs, and forms where the integrated range field feels cramped.
Selected: —
"use client";
import { useState } from "react";
import type { DateRangeValue } from "@/components/f-ui/date-internals/date-value";
import { DateRangePicker } from "@/components/f-ui/date-range-picker/date-range-picker";
export function DateRangePickerSplitDemo() {
const [range, setRange] = useState<DateRangeValue | undefined>(undefined);
return (
<div className="max-w-xs space-y-3">
<DateRangePicker layout="split" value={range} onChange={setRange} showToday />
<p className="text-muted-foreground text-xs">
Selected:{" "}
<span className="text-foreground font-medium tabular-nums">
{range?.from && range?.to
? `${range.from.toLocaleDateString()} – ${range.to.toLocaleDateString()}`
: range?.from
? `${range.from.toLocaleDateString()} – …`
: "—"}
</span>
</p>
</div>
);
}Integrated-only props (presets, confirmable, visibleMonths) do not apply in split mode.
Inside a Dialog
Use layout="split" inside shadcn Dialog so each endpoint has its own field and calendar. Raise popovers above the dialog with classNames.startPopover and classNames.endPopover.
Selected: —
"use client";
import { useState } from "react";
import type { DateRangeValue } from "@/components/f-ui/date-internals/date-value";
import { DateRangePicker } from "@/components/f-ui/date-range-picker/date-range-picker";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
function formatRange(range: DateRangeValue | undefined): string {
if (!range?.from && !range?.to) return "—";
const fmt = (d: Date) => d.toLocaleDateString();
if (range.from && range.to) return `${fmt(range.from)} – ${fmt(range.to)}`;
if (range.from) return `${fmt(range.from)} – …`;
return `… – ${fmt(range.to!)}`;
}
export function DateRangePickerDialogDemo() {
const [range, setRange] = useState<DateRangeValue | undefined>(undefined);
return (
<div className="max-w-xl space-y-3">
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Pick range in dialog</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Reporting period</DialogTitle>
<DialogDescription>
Choose a start and end date inside this dialog.
</DialogDescription>
</DialogHeader>
<DateRangePicker
layout="split"
value={range}
onChange={setRange}
showToday
classNames={{
startPopover: "z-[60]",
endPopover: "z-[60]",
}}
/>
</DialogContent>
</Dialog>
<p className="text-muted-foreground text-xs">
Selected:{" "}
<span className="text-foreground font-medium tabular-nums">
{formatRange(range)}
</span>
</p>
</div>
);
}Headless Usage
Use useDateRangePicker for all picker state — open/close, field props, calendar props, presets, and clear. Build your own shell with plain HTML (or your design system). Wire DateRangePickerControl and Calendar from f-ui; use DateRangePickerPopover so the VCP grid mounts in a Radix popover panel.
Committed value: —
"use client";
import { useState } from "react";
import { Calendar } from "@/components/f-ui/calendar/calendar";
import type { DateRangeValue } from "@/components/f-ui/date-internals/date-value";
import {
buildDefaultDateRangePresets,
useDateRangePicker,
} from "@/components/f-ui/date-range-picker/date-range-picker";
import { DateRangePickerControl } from "@/components/f-ui/date-range-picker/date-range-picker-parts/control";
import { DateRangePickerPopover } from "@/components/f-ui/date-range-picker/date-range-picker-parts/popover";
import { DateRangePickerTrigger } from "@/components/f-ui/date-range-picker/date-range-picker-parts/trigger";
import { useDateRangePickerI18n } from "@/components/f-ui/date-range-picker/hooks/use-date-range-picker-i18n";
import { Popover, PopoverAnchor, PopoverTrigger } from "@/components/ui/popover";
export function DateRangePickerHeadlessDemo() {
const [range, setRange] = useState<DateRangeValue | undefined>(undefined);
const { t, locale } = useDateRangePickerI18n();
const presets = buildDefaultDateRangePresets(t);
const picker = useDateRangePicker({
value: range,
onChange: setRange,
locale,
visibleMonths: 2,
showClear: true,
});
const committedLabel =
range?.from && range?.to
? `${range.from.toLocaleDateString(locale)} – ${range.to.toLocaleDateString(locale)}`
: "—";
return (
<Popover open={picker.isOpen} onOpenChange={picker.onOpenChange}>
<div className="max-w-xl">
<label htmlFor="headless-date-range-picker">Date range</label>
<PopoverAnchor asChild>
<div id="headless-date-range-picker" className="mt-1 min-w-0">
<DateRangePickerControl
{...picker.fieldProps}
startAriaLabel={t("startAriaLabel")}
endAriaLabel={t("endAriaLabel")}
calendarTrigger={
<PopoverTrigger asChild>
<DateRangePickerTrigger
aria-label={t("openCalendar")}
onClick={picker.onTriggerClick}
/>
</PopoverTrigger>
}
/>
</div>
</PopoverAnchor>
<p className="mt-2 text-sm">
Committed value: <strong>{committedLabel}</strong>
</p>
</div>
<DateRangePickerPopover className="flex w-fit max-w-[min(100vw-2rem,100%)] flex-col gap-0 overflow-hidden p-0 sm:flex-row">
<ul className="m-0 shrink-0 list-none p-2 sm:border-r sm:p-2">
<li className="text-muted-foreground mb-1 text-xs font-medium uppercase">
{t("presetsHeading")}
</li>
{presets.map((preset) => (
<li key={preset.id}>
<button
type="button"
className="block w-full px-2 py-1 text-left text-sm"
onClick={() => picker.onPresetSelect(preset.getRange(new Date()))}
>
{preset.label}
</button>
</li>
))}
</ul>
<div className="w-fit shrink-0">
<Calendar
{...picker.calendarProps}
popoverOpen={picker.isOpen}
onDismiss={() => picker.setIsOpen(false)}
clearLabel={picker.showClearButton ? t("clear") : undefined}
onClear={picker.showClearButton ? picker.onClear : undefined}
classNames={{ root: "border-0 bg-transparent shadow-none" }}
/>
</div>
</DateRangePickerPopover>
</Popover>
);
}The hook returns ready-to-spread props so you do not reimplement range commit rules, partial selection, withTime draft, or confirmable apply yourself.
Composition
Stock Component
DateRangePicker
├── Label (optional)
├── DateRangePickerControl (segmented start/end + calendar icon)
│ └── PopoverTrigger → DateRangePickerTrigger
├── DateRangePickerPopover
│ ├── DateRangePickerPresets (optional)
│ └── Calendar mode="range"
└── Clear in Calendar footer (optional)Headless
useDateRangePicker
├── fieldProps → DateRangePickerControl (your layout; pass calendarTrigger)
├── calendarProps → Calendar (your panel)
├── onPresetSelect → your preset buttons
├── onClear → or Calendar footer via clearLabel/onClear
└── isOpen / onOpenChange / onTriggerClickAPI Reference
Props
| Prop | Type | Default |
|---|---|---|
value | DateRangeValue | — |
onChange | (range: DateRangeValue | undefined) => void | — |
onBlur | () => void | — |
locale | string | i18n/provider locale |
withTime | boolean | false |
confirmable | boolean | false |
visibleMonths | 1 | 2 | 2 |
layout | "inline" | "stacked" | "inline" |
minValue / maxValue | Date | — |
isDateUnavailable | (date: Date) => boolean | — |
firstDayOfWeek | 0 | 1 | 2 | 3 | 4 | 5 | 6 | locale default |
disabled | boolean | false |
isInvalid | boolean | false |
label | ReactNode | — |
aria-label | string | localized ariaLabel |
description / errorMessage | ReactNode | — |
showToday | boolean | calendar default |
showClear | boolean | false |
presets | DateRangePresetItem[] | — |
className | string | — |
classNames | Partial<Record<DateRangePickerSlot, string>> | — |
t | DateRangePickerTranslateFn | built-in/provider |
buildDefaultDateRangePresets(t) creates a localized baseline preset list (today, yesterday, last 7/30 days, month/year ranges).
Slots
| Slot | Element |
|---|---|
root | Outer wrapper |
field | Field wrapper |
input | Start/end segmented inputs inside control |
icon | Calendar trigger button |
popover | Popover panel |
calendar | Embedded Calendar |
presets | Preset list wrapper |
presetItem | Preset button |
clear | Clear action (Calendar footer) |
description | Description text |
errorMessage | Error text |
Hook — useDateRangePicker(options)
| Option | Type |
|---|---|
value | DateRangeValue |
onChange | (range: DateRangeValue | undefined) => void |
onBlur | () => void |
locale | string (required) |
withTime | boolean |
confirmable | boolean |
visibleMonths | 1 | 2 |
minValue / maxValue | Date |
isDateUnavailable | (date: Date) => boolean |
firstDayOfWeek | 0–6 |
disabled | boolean |
isInvalid | boolean |
showClear | boolean |
| Return | Type |
|---|---|
value | committed DateRangeValue |
displayRange | range shown in field / presets highlight |
isOpen / setIsOpen | popover open state |
onOpenChange | (open: boolean) => void |
onTriggerClick | toggle handler for your trigger button |
fieldProps | spread on DateRangePickerControl (omit calendarTrigger) |
calendarProps | spread on Calendar (mode: "range") |
showClearButton | boolean |
onClear | clear committed value + close |
onPresetSelect | (range: DateRangeValue) => void |