- Accordion
- AppShell
- Autocomplete
- Badge
- Blobatar
- BoringAvatar
- Button
- Calendar
- Checkbox
- DataGrid
- Date Picker
- Date Range Picker
- DateTime
- Dialog
- Drawer
- Dropdown
- Empty
- Error State
- Frame
- Input
- Input Amount
- Input Group
- Input OTP
- Input Password
- Input Phone
- Input Search
- Input Secret
- Loading
- PageContainer
- PageHeader
- Popover
- Select
- StatsCard
- Switch
- Tabs
- TextArea
- Toast
- Tooltip
Playground
"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:
- date-fns
- lucide-react
- react-day-picker
$ pnpm add date-fns lucide-react react-day-pickerUsage
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.
"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
"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
| Prop | Type / Description | Default |
|---|---|---|
| motion | booleanEnables popup, calendar, and button motion; system reduced-motion takes priority. | true |
| applyLabel | stringDateRangePicker: apply button text. | "Apply" |
| cancelLabel | stringDateRangePicker: cancel button text. | "Cancel" |
| className | stringDateRangePicker: trigger class name. | undefined |
| clearLabel | stringDateRangePicker: clear button text. | "Clear" |
| clearable | booleanDateRangePicker: adds a transactional Clear action. | false |
| dateFormat | stringDateRangePicker: date-fns display format. | "PPP" |
| defaultOpen | booleanDateRangePicker: initial uncontrolled popover visibility. | false |
| defaultValue | DateRange | undefinedDateRangePicker: initial uncontrolled range. | undefined |
| disabled | booleanDateRangePicker: disables opening the trigger. | false |
| disabledDates | readonly Date[]DateRangePicker: local dates excluded from selection. | [] |
| endMonth | DateDateRangePicker: latest navigable month. | undefined |
| features | readonly ("twoMonths" | "shortcuts")[]DateRangePicker: optional second month and shortcut list. | [] |
| id | stringDateRangePicker: optional stable id for the label relationship. | generated |
| label | stringDateRangePicker: visible accessible name. | required |
| locale | date-fns LocaleDateRangePicker: display and calendar locale. | undefined |
| now | () => DateDateRangePicker: clock used to resolve shortcuts. | current date |
| onOpenChange | (open: boolean) => voidDateRangePicker: observes popover visibility changes. | undefined |
| onValueChange | (range: DateRange | undefined) => voidDateRangePicker: fires only after Apply. | undefined |
| open | booleanDateRangePicker: controlled popover visibility. | uncontrolled |
| placeholder | stringDateRangePicker: trigger text when empty. | "Select dates" |
| readOnly | booleanDateRangePicker: opens the calendar but prevents changes. | false |
| shortcuts | readonly DateShortcut[]DateRangePicker: which named shortcut choices are shown. | all shortcuts |
| startMonth | DateDateRangePicker: earliest navigable month. | undefined |
| value | DateRange | undefinedDateRangePicker: committed inclusive local-day range. | uncontrolled |
| weekStartsOn | 0 | 1 | 2 | 3 | 4 | 5 | 6DateRangePicker: 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.