# Date Range Picker

A transactional date-range picker with optional calendar shortcuts.

> For the complete documentation index, see [llms.txt](/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](/.well-known/agent-skills/site-skill.md).





## Playground [#playground]

```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 [#installation]

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

```bash
npx 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 [#dependencies]

Installed automatically by the registry CLI. For manual setup:

```bash
npm install date-fns lucide-react react-day-picker
```

## Usage [#usage]

```tsx
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 [#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) [#storybook-stories-optional]

Install DateRangePicker first, then:

```bash
npx 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 [#examples]

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

<ComponentPreview name="date-range-picker-demo" title="date-range-picker-demo.tsx">
  <DateRangePickerDemo />
</ComponentPreview>

## Behavior & Accessibility [#behavior--accessibility]

### Indonesian calendar and action labels [#indonesian-calendar-and-action-labels]

<ComponentPreview name="date-range-picker-localized" title="date-range-picker-localized.tsx">
  <DateRangePickerLocalized />
</ComponentPreview>

`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 [#props]

| Prop          | Type                                     | Default        | Description                                                                       |
| ------------- | ---------------------------------------- | -------------- | --------------------------------------------------------------------------------- |
| motion        | boolean                                  | true           | Enables popup, calendar, and button motion; system reduced-motion takes priority. |
| applyLabel    | string                                   | "Apply"        | DateRangePicker: apply button text.                                               |
| cancelLabel   | string                                   | "Cancel"       | DateRangePicker: cancel button text.                                              |
| className     | string                                   | undefined      | DateRangePicker: trigger class name.                                              |
| clearLabel    | string                                   | "Clear"        | DateRangePicker: clear button text.                                               |
| clearable     | boolean                                  | false          | DateRangePicker: adds a transactional Clear action.                               |
| dateFormat    | string                                   | "PPP"          | DateRangePicker: date-fns display format.                                         |
| defaultOpen   | boolean                                  | false          | DateRangePicker: initial uncontrolled popover visibility.                         |
| defaultValue  | DateRange \| undefined                   | undefined      | DateRangePicker: initial uncontrolled range.                                      |
| disabled      | boolean                                  | false          | DateRangePicker: disables opening the trigger.                                    |
| disabledDates | readonly Date\[]                         | \[]            | DateRangePicker: local dates excluded from selection.                             |
| endMonth      | Date                                     | undefined      | DateRangePicker: latest navigable month.                                          |
| features      | readonly ("twoMonths" \| "shortcuts")\[] | \[]            | DateRangePicker: optional second month and shortcut list.                         |
| id            | string                                   | generated      | DateRangePicker: optional stable id for the label relationship.                   |
| label         | string                                   | required       | DateRangePicker: visible accessible name.                                         |
| locale        | date-fns Locale                          | undefined      | DateRangePicker: display and calendar locale.                                     |
| now           | () => Date                               | current date   | DateRangePicker: clock used to resolve shortcuts.                                 |
| onOpenChange  | (open: boolean) => void                  | undefined      | DateRangePicker: observes popover visibility changes.                             |
| onValueChange | (range: DateRange \| undefined) => void  | undefined      | DateRangePicker: fires only after Apply.                                          |
| open          | boolean                                  | uncontrolled   | DateRangePicker: controlled popover visibility.                                   |
| placeholder   | string                                   | "Select dates" | DateRangePicker: trigger text when empty.                                         |
| readOnly      | boolean                                  | false          | DateRangePicker: opens the calendar but prevents changes.                         |
| shortcuts     | readonly DateShortcut\[]                 | all shortcuts  | DateRangePicker: which named shortcut choices are shown.                          |
| startMonth    | Date                                     | undefined      | DateRangePicker: earliest navigable month.                                        |
| value         | DateRange \| undefined                   | uncontrolled   | DateRangePicker: committed inclusive local-day range.                             |
| weekStartsOn  | 0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6          | 1              | DateRangePicker: first day for calendar and week shortcuts.                       |

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 [#source]

```tsx
"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 [#credits]

* [shadcn/ui](https://ui.shadcn.com/docs/components/base/date-picker): Date range Button, Popover, and Calendar composition; Vandor adds transactional Apply/Cancel, shortcuts, and motion controls.
