f-ui
Components

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

EventBehavior
Calendar trigger clickToggle popover
Focus any range segmentOpen popover
Edit segments while popover is openPopover 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 clickCommit 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 / EscapeClose popover (withTime commits draft on close)

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.

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.

mmddyyyy
mmddyyyy
"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).

mmddyyyy
mmddyyyy
"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.

mmddyyyy
mmddyyyy
"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.

mmddyyyy––––AM
mmddyyyy––––AM
"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.

mmddyyyy
mmddyyyy
"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.

mmddyyyy
mmddyyyy

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.

mmddyyyy
mmddyyyy

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

API Reference

Props

PropTypeDefault
valueDateRangeValue
onChange(range: DateRangeValue | undefined) => void
onBlur() => void
localestringi18n/provider locale
withTimebooleanfalse
confirmablebooleanfalse
visibleMonths1 | 22
layout"inline" | "stacked""inline"
minValue / maxValueDate
isDateUnavailable(date: Date) => boolean
firstDayOfWeek0 | 1 | 2 | 3 | 4 | 5 | 6locale default
disabledbooleanfalse
isInvalidbooleanfalse
labelReactNode
aria-labelstringlocalized ariaLabel
description / errorMessageReactNode
showTodaybooleancalendar default
showClearbooleanfalse
presetsDateRangePresetItem[]
classNamestring
classNamesPartial<Record<DateRangePickerSlot, string>>
tDateRangePickerTranslateFnbuilt-in/provider

buildDefaultDateRangePresets(t) creates a localized baseline preset list (today, yesterday, last 7/30 days, month/year ranges).

Slots

SlotElement
rootOuter wrapper
fieldField wrapper
inputStart/end segmented inputs inside control
iconCalendar trigger button
popoverPopover panel
calendarEmbedded Calendar
presetsPreset list wrapper
presetItemPreset button
clearClear action (Calendar footer)
descriptionDescription text
errorMessageError text

Hook — useDateRangePicker(options)

OptionType
valueDateRangeValue
onChange(range: DateRangeValue | undefined) => void
onBlur() => void
localestring (required)
withTimeboolean
confirmableboolean
visibleMonths1 | 2
minValue / maxValueDate
isDateUnavailable(date: Date) => boolean
firstDayOfWeek0–6
disabledboolean
isInvalidboolean
showClearboolean
ReturnType
valuecommitted DateRangeValue
displayRangerange shown in field / presets highlight
isOpen / setIsOpenpopover open state
onOpenChange(open: boolean) => void
onTriggerClicktoggle handler for your trigger button
fieldPropsspread on DateRangePickerControl (omit calendarTrigger)
calendarPropsspread on Calendar (mode: "range")
showClearButtonboolean
onClearclear committed value + close
onPresetSelect(range: DateRangeValue) => void

On this page