For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to any URL or sending an Accept: text/markdown header. An agent skill is available at /.well-known/agent-skills/site-skill.md.
1
Support

Date Range Picker

A transactional date-range picker with optional calendar shortcuts.

Playground

Live preview
Choose dates as a draft, then Apply. Cancel, Escape, and outside dismissal discard changes.
Customize date-range-picker
Customize
date-range-picker-demo.tsx
"use client";

import { useState } from "react";
import type { DateRange } from "react-day-picker";
import { DateRangePicker } from "@/components/ui/date-range-picker";

export function DateRangePickerDemo() {
  const [range, setRange] = useState<DateRange>();
  return (
    <>
      <DateRangePicker
          label={"Reporting period"}
          value={range}
          onValueChange={setRange}
          placeholder={"Select dates"}
          motion={true}
          weekStartsOn={1}
          features={["shortcuts"]}
      />
    </>
  );
}

Installation

Initialize shadcn in your React project, then install this component from the Vandor UI registry:

$ pnpm dlx shadcn@latest add https://vandor-ui.vercel.app/r/date-range-picker.json

The default installation target is components/ui/date-range-picker.tsx. Review the CLI's overwrite prompt before replacing local customizations.

Dependencies

Installed automatically by the registry CLI. For manual setup:

Dependencies3 required packages
  • date-fns
  • lucide-react
  • react-day-picker
$ pnpm add date-fns lucide-react react-day-picker

Usage

import { DateRangePicker } from "@/components/ui/date-range-picker";
 
<DateRangePicker
  label="Reporting period"
  value={range}
  onValueChange={setRange}
  features={["twoMonths", "shortcuts"]}
/>;

Choose twoMonths and shortcuts independently. Shortcut ranges include today; weeks begin on Monday by default. Dates represent local calendar days, and range endpoints are inclusive.

This extends shadcn's range composition with Vandor's animated Popover and existing transactional behavior. motion={false} disables popup, Calendar, and all button motion. System reduced-motion preferences take priority. Popup reveal is 180ms and exit 120ms; draft/commit callbacks do not wait for animation.

Consumer setup

Requires React 19.2+, Tailwind CSS 4 and shadcn theme tokens. CLI installation includes Button (and Loading files), Calendar, Popover, and date-picker-utils.ts. Review overwrite prompts for customized primitives. Copying Source alone is insufficient: manual installation also needs these registered files/dependencies. No separate animation CSS is required.

Storybook stories (optional)

Install DateRangePicker first, then:

$ pnpm dlx shadcn@latest add https://vandor-ui.vercel.app/r/date-range-picker-stories.json

The stories-only item does not reinstall components, packages, or Storybook configuration. Discover your UI directory using **/*.stories.@(js|jsx|mjs|ts|tsx) and import your application's global Tailwind CSS in the existing preview. Stories are typed/composed with @storybook/react 10.6.1; align your Storybook packages. autodocs requires the existing docs addon. Consumer themes/decorators are inherited.

Examples

The controlled value changes only on Apply. Try a shortcut, alter its draft, then cancel to discard it.

date-range-picker-demo.tsx
"use client";

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

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

export const DateRangePickerDemo = () => {
  const [range, setRange] = useState<DateRange>();
  return (
    <DateRangePicker
      label="Reporting period"
      value={range}
      onValueChange={setRange}
      features={["twoMonths", "shortcuts"]}
      clearable
    />
  );
};

Behavior & Accessibility

Indonesian calendar and action labels

date-range-picker-localized.tsx
"use client";

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

export const DateRangePickerLocalized = () => (
  <DateRangePicker
    label="Periode laporan"
    placeholder="Pilih tanggal"
    locale={dateRangeIndonesianLocale}
    applyLabel="Terapkan"
    cancelLabel="Batal"
    clearLabel="Hapus"
    clearable
    defaultValue={{ from: new Date(2026, 9, 1), to: new Date(2026, 9, 7) }}
  />
);

dateRangeIndonesianLocale is re-exported from the public react-day-picker/locale entry. It localizes calendar/date formatting; application copy remains caller-owned (label, placeholder, applyLabel, cancelLabel, clearLabel). Shortcut labels remain English, so this localized example intentionally omits shortcuts. The value is still a pair of local calendar dates, not a timezone-aware instant.

The popup opens at the committed range's starting month and focuses an eligible date. Label and displayed value name the trigger; the popup has an accessible title. Two-month mode shows one month below 768px. Popup scrolling respects the available viewport height.

value (including explicit undefined) is controlled; otherwise defaultValue initializes internal state. open controls visibility independently. Read-only permits inspection/navigation, disables shortcuts/clear/Apply, and leaves Cancel available. Clearing changes the draft only; Apply commits undefined. Incomplete or disabled-date ranges cannot be applied.

The picker is not a native form field; serialize committed values explicitly. Dates are local calendar days; formatting is display-only. Generated playground code reproduces configuration, not transient draft/selection. Reset clears the value and closes the popup.

Calendar edits remain a draft until Apply. Cancel, Escape, and outside dismissal discard the draft. Apply is disabled for incomplete, reversed, or disabled-date ranges. Clearing is an explicit draft when clearable is enabled.

Props

Component props, types, defaults, and descriptions
PropType / DescriptionDefault
motionboolean

Enables popup, calendar, and button motion; system reduced-motion takes priority.

true
applyLabelstring

DateRangePicker: apply button text.

"Apply"
cancelLabelstring

DateRangePicker: cancel button text.

"Cancel"
classNamestring

DateRangePicker: trigger class name.

undefined
clearLabelstring

DateRangePicker: clear button text.

"Clear"
clearableboolean

DateRangePicker: adds a transactional Clear action.

false
dateFormatstring

DateRangePicker: date-fns display format.

"PPP"
defaultOpenboolean

DateRangePicker: initial uncontrolled popover visibility.

false
defaultValueDateRange | undefined

DateRangePicker: initial uncontrolled range.

undefined
disabledboolean

DateRangePicker: disables opening the trigger.

false
disabledDatesreadonly Date[]

DateRangePicker: local dates excluded from selection.

[]
endMonthDate

DateRangePicker: latest navigable month.

undefined
featuresreadonly ("twoMonths" | "shortcuts")[]

DateRangePicker: optional second month and shortcut list.

[]
idstring

DateRangePicker: optional stable id for the label relationship.

generated
labelstring

DateRangePicker: visible accessible name.

required
localedate-fns Locale

DateRangePicker: display and calendar locale.

undefined
now() => Date

DateRangePicker: clock used to resolve shortcuts.

current date
onOpenChange(open: boolean) => void

DateRangePicker: observes popover visibility changes.

undefined
onValueChange(range: DateRange | undefined) => void

DateRangePicker: fires only after Apply.

undefined
openboolean

DateRangePicker: controlled popover visibility.

uncontrolled
placeholderstring

DateRangePicker: trigger text when empty.

"Select dates"
readOnlyboolean

DateRangePicker: opens the calendar but prevents changes.

false
shortcutsreadonly DateShortcut[]

DateRangePicker: which named shortcut choices are shown.

all shortcuts
startMonthDate

DateRangePicker: earliest navigable month.

undefined
valueDateRange | undefined

DateRangePicker: committed inclusive local-day range.

uncontrolled
weekStartsOn0 | 1 | 2 | 3 | 4 | 5 | 6

DateRangePicker: first day for calendar and week shortcuts.

1

The reference lists component-specific props and selected native props, not the complete React API. Other native attributes are forwarded to the rendered element.

Source

"use client";
 
import { cn } from "cn";
import { format } from "date-fns";
import type { Locale } from "date-fns";
import { CalendarDays } from "lucide-react";
import * as React from "react";
import type { DateRange, Matcher } from "react-day-picker";
import { id as indonesianLocale } from "react-day-picker/locale";
 
import { Button } from "./button";
import { Calendar } from "./calendar";
import {
  isSelectableDateRange,
  resolveDateShortcut,
} from "./date-picker-utils";
import type { DateShortcut } from "./date-picker-utils";
import {
  Popover,
  PopoverContent,
  PopoverTitle,
  PopoverTrigger,
} from "./popover";
 
// shadcn Date Picker composition, extended with transactional range selection.
 
export type DateRangeFeature = "twoMonths" | "shortcuts";
 
export const dateRangeIndonesianLocale = indonesianLocale;
 
const allShortcuts: readonly DateShortcut[] = [
  "last7Days",
  "last14Days",
  "last30Days",
  "thisWeek",
  "lastWeek",
  "thisMonth",
  "lastMonth",
];
 
const shortcutLabels: Record<DateShortcut, string> = {
  last14Days: "Last 14 days",
  last30Days: "Last 30 days",
  last7Days: "Last 7 days",
  lastMonth: "Last month",
  lastWeek: "Last week",
  thisMonth: "This month",
  thisWeek: "This week",
};
 
export interface DateRangePickerProps {
  id?: string;
  label: string;
  placeholder?: string;
  value?: DateRange;
  defaultValue?: DateRange;
  onValueChange?: (range: DateRange | undefined) => void;
  locale?: Locale;
  dateFormat?: string;
  disabled?: boolean;
  readOnly?: boolean;
  open?: boolean;
  defaultOpen?: boolean;
  onOpenChange?: (open: boolean) => void;
  startMonth?: Date;
  endMonth?: Date;
  disabledDates?: readonly Date[];
  features?: readonly DateRangeFeature[];
  shortcuts?: readonly DateShortcut[];
  now?: () => Date;
  weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
  clearable?: boolean;
  applyLabel?: string;
  cancelLabel?: string;
  clearLabel?: string;
  className?: string;
  motion?: boolean;
}
 
const getShownValue = (
  committed: DateRange | undefined,
  dateFormat: string,
  locale: Locale | undefined,
  placeholder: string
) =>
  committed?.from
    ? `${format(committed.from, dateFormat, { locale })}${committed.to ? ` – ${format(committed.to, dateFormat, { locale })}` : ""}`
    : placeholder;
 
const getCurrentDate = () => new Date();
 
const withRangePickerDefaults = ({
  placeholder = "Select dates",
  dateFormat = "PPP",
  defaultOpen = false,
  disabledDates = [],
  features = [],
  shortcuts = allShortcuts,
  now = getCurrentDate,
  weekStartsOn = 1,
  clearable = false,
  applyLabel = "Apply",
  cancelLabel = "Cancel",
  clearLabel = "Clear",
  motion = true,
  ...props
}: DateRangePickerProps) => ({
  ...props,
  applyLabel,
  cancelLabel,
  clearLabel,
  clearable,
  dateFormat,
  defaultOpen,
  disabledDates,
  features,
  motion,
  now,
  placeholder,
  shortcuts,
  weekStartsOn,
});
 
export const DateRangePicker = (props: DateRangePickerProps) => {
  const {
    id,
    label,
    placeholder,
    value,
    defaultValue,
    onValueChange,
    locale,
    dateFormat,
    disabled,
    readOnly,
    open,
    defaultOpen,
    onOpenChange,
    startMonth,
    endMonth,
    disabledDates,
    features,
    shortcuts,
    now,
    weekStartsOn,
    clearable,
    applyLabel,
    cancelLabel,
    clearLabel,
    className,
    motion,
  } = withRangePickerDefaults(props);
  const controlled = Object.hasOwn(props, "value");
  const generatedId = React.useId();
  const triggerId = id ?? generatedId;
  const [internalValue, setInternalValue] = React.useState(defaultValue);
  const committed = controlled ? value : internalValue;
  const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
  const isOpen = open === undefined ? internalOpen : open;
  const [draft, setDraft] = React.useState<DateRange | undefined>(committed);
  const [clearedDraft, setClearedDraft] = React.useState(false);
  const [wideViewport, setWideViewport] = React.useState(false);
 
  React.useEffect(() => {
    const media = window.matchMedia("(min-width: 768px)");
    const update = () => setWideViewport(media.matches);
    update();
    media.addEventListener("change", update);
    return () => media.removeEventListener("change", update);
  }, []);
 
  const syncControlledDraft = React.useEffectEvent(() => {
    if (controlled) {
      setDraft(committed);
      setClearedDraft(false);
    }
  });
  const fromTime = committed?.from?.getTime();
  const toTime = committed?.to?.getTime();
  React.useEffect(() => {
    syncControlledDraft();
  }, [controlled, fromTime, toTime]);
 
  const setOpen = (next: boolean) => {
    setDraft(committed);
    setClearedDraft(false);
    if (open === undefined) {
      setInternalOpen(next);
    }
    onOpenChange?.(next);
  };
 
  const dateDisabled = (date: Date) =>
    disabledDates.some(
      (item) =>
        item.getFullYear() === date.getFullYear() &&
        item.getMonth() === date.getMonth() &&
        item.getDate() === date.getDate()
    );
  const matcher: Matcher[] = [dateDisabled];
  if (readOnly) {
    matcher.push(() => true);
  }
  const canApply =
    !readOnly && (clearedDraft || isSelectableDateRange(draft, disabledDates));
  const shownValue = getShownValue(committed, dateFormat, locale, placeholder);
  const whileTap = motion ? { scale: 0.96 } : false;
 
  return (
    <div data-slot="date-range-picker" className="grid min-w-0 gap-1.5">
      <label
        htmlFor={triggerId}
        id={`${triggerId}-label`}
        className="text-sm font-medium"
      >
        {label}
      </label>
      <Popover open={isOpen} onOpenChange={setOpen}>
        <PopoverTrigger
          id={triggerId}
          disabled={disabled}
          aria-labelledby={`${triggerId}-label ${triggerId}-value`}
          render={<Button variant="outline" whileTap={whileTap} />}
          className={cn(
            "inline-flex h-9 min-w-0 items-center justify-start gap-2 rounded-md border border-input bg-background px-3 text-sm font-normal shadow-xs outline-none hover:bg-accent focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 dark:bg-input/30",
            className
          )}
        >
          <CalendarDays aria-hidden="true" className="size-4 shrink-0" />
          <span
            id={`${triggerId}-value`}
            className={cn(
              "truncate",
              !committed?.from && "text-muted-foreground"
            )}
          >
            {shownValue}
          </span>
        </PopoverTrigger>
        <PopoverContent
          align="start"
          animated={motion}
          className="w-auto gap-0 p-2"
        >
          <PopoverTitle className="sr-only">{label}</PopoverTitle>
          <div className="flex flex-col gap-3 sm:flex-row">
            {features.includes("shortcuts") ? (
              <div className="flex flex-wrap gap-1 sm:w-32 sm:flex-col sm:flex-nowrap">
                {shortcuts.map((shortcut) => (
                  <Button
                    key={shortcut}
                    type="button"
                    variant="ghost"
                    size="sm"
                    whileTap={whileTap}
                    className="justify-start whitespace-nowrap"
                    disabled={readOnly}
                    onClick={() => {
                      setDraft(
                        resolveDateShortcut(shortcut, now(), weekStartsOn)
                      );
                      setClearedDraft(false);
                    }}
                  >
                    {shortcutLabels[shortcut]}
                  </Button>
                ))}
              </div>
            ) : null}
            <Calendar
              mode="range"
              motion={motion}
              autoFocus
              defaultMonth={draft?.from}
              locale={locale}
              selected={draft}
              numberOfMonths={
                features.includes("twoMonths") && wideViewport ? 2 : 1
              }
              disabled={matcher}
              startMonth={startMonth}
              endMonth={endMonth}
              weekStartsOn={weekStartsOn}
              onSelect={(range) => {
                setDraft(range);
                setClearedDraft(false);
              }}
            />
          </div>
          <div className="mt-2 flex justify-end gap-2 border-t pt-2">
            {clearable ? (
              <Button
                type="button"
                variant="ghost"
                whileTap={whileTap}
                disabled={readOnly}
                onClick={() => {
                  setDraft(undefined);
                  setClearedDraft(true);
                }}
              >
                {clearLabel}
              </Button>
            ) : null}
            <Button
              type="button"
              variant="outline"
              whileTap={whileTap}
              onClick={() => setOpen(false)}
            >
              {cancelLabel}
            </Button>
            <Button
              type="button"
              disabled={!canApply}
              whileTap={whileTap}
              onClick={() => {
                const next = clearedDraft ? undefined : draft;
                if (!controlled) {
                  setInternalValue(next);
                }
                onValueChange?.(next);
                setOpen(false);
              }}
            >
              {applyLabel}
            </Button>
          </div>
        </PopoverContent>
      </Popover>
    </div>
  );
};
 

Credits

  • shadcn/ui: Date range Button, Popover, and Calendar composition; Vandor adds transactional Apply/Cancel, shortcuts, and motion controls.