- Accordion
- AppShell
- Autocomplete
- Badge
- Blobatar
- BoringAvatar
- Button
- Calendar
- Checkbox
- DataGrid
- Date Picker
- Date Range Picker
- DateTime
- Date Time Picker
- Date Time Range Picker
- 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 { DateTimePicker } from "@/components/ui/date-time-picker";
export function DateTimePickerDemo() {
const [date, setDate] = useState<Date>();
return (
<>
<DateTimePicker
label={"Starts at"}
value={date}
onValueChange={setDate}
placeholder={"Select date and time"}
dateFormat={"PPP p"}
precision={"minute"}
hourCycle={12}
motion={true}
/>
</>
);
}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-time-picker.json
Dependencies
Installed automatically by the registry CLI. For manual setup:
- cn@^0.4.0
- date-fns@^4.4.0
- lucide-react@1.11.0
- react-day-picker@^10.0.2
$ pnpm add cn@^0.4.0 date-fns@^4.4.0 lucide-react@1.11.0 react-day-picker@^10.0.2Copying the Source block alone is insufficient. Use the CLI installation above to include all registry dependencies and supporting files; the npm command alone does not install them.
Required registry dependencies: https://vandor-ui.vercel.app/r/button.json, https://vandor-ui.vercel.app/r/calendar.json, https://vandor-ui.vercel.app/r/popover.json.
Required source and supporting files: registry/new-york/date-time-picker.tsx, registry/new-york/date-time-picker-parts.tsx, registry/new-york/date-time-picker-utils.ts.
Usage
import { DateTimePicker } from "@/components/ui/date-time-picker";
<DateTimePicker label="Starts at" value={date} onValueChange={setDate} />;The value is a JavaScript Date holding a local calendar day and a local time of day. Choose a day and a time as a draft, then press Apply to commit. Set clearable to add a Clear action that commits an empty value immediately.
This is not the DateTime component, which only displays an instant. DateTimePicker follows the Date Picker conventions but commits transactionally, like the Date Range Picker. motion={false} disables popup, Calendar, and button animations; system reduced-motion preferences take priority.
Consumer setup
Requires React 19.2+, Tailwind CSS 4 with shadcn theme tokens, and the declared dependencies. CLI installation includes Button (and its Loading files), Calendar, and Popover; review existing primitive overwrite prompts. Copying Source alone is insufficient for manual installation: also copy date-time-picker-parts.tsx and date-time-picker-utils.ts and install the registry dependencies. No additional animation stylesheet is needed.
Storybook stories (optional)
Install DateTimePicker first, then:
$ pnpm dlx shadcn@latest add https://vandor-ui.vercel.app/r/date-time-picker-stories.json
Only the stories file is installed; no component or Storybook configuration is overwritten. Add your UI directory to your existing **/*.stories.@(js|jsx|mjs|ts|tsx) discovery pattern and import your app's global Tailwind CSS in Storybook preview. Stories use @storybook/react 10.6.1 types/composition; keep Storybook packages aligned. autodocs needs your existing docs addon. Stories inherit consumer themes.
Examples
This controlled picker offers minute precision and a clear action.
"use client";
import { useState } from "react";
import { DateTimePicker } from "@/components/ui/date-time-picker";
export const DateTimePickerDemo = () => {
const [date, setDate] = useState<Date>();
return (
<DateTimePicker
label="Starts at"
value={date}
onValueChange={setDate}
clearable
/>
);
};Set precision="second" to add a seconds column. Match dateFormat so the trigger shows them too.
"use client";
import { useState } from "react";
import { DateTimePicker } from "@/components/ui/date-time-picker";
export const DateTimePickerSecondsDemo = () => {
const [date, setDate] = useState<Date | undefined>(
new Date(2026, 9, 5, 14, 30, 15)
);
return (
<DateTimePicker
label="Backup window"
value={date}
onValueChange={setDate}
precision="second"
dateFormat="PPP HH:mm:ss"
/>
);
};Behavior & Accessibility
Picking a day or a time changes a draft only. onValueChange fires on Apply with the merged day and time, and on Clear with undefined. Cancel, Escape, or an outside click discards the draft; reopening always starts from the committed value. Apply is disabled until a day is chosen; the time always holds a complete value. Enter inside a time column applies.
Emitted dates always have zero milliseconds, and zero seconds at the default minute precision. A controlled value with extra precision is displayed truncated and is not mutated.
disabled prevents opening and closes an open popup. readOnly shows the value and lets the popup open, but the calendar, time field, Apply, and Clear cannot change anything. value (including explicit undefined) is controlled; otherwise defaultValue initializes internal state. open/onOpenChange independently control visibility. disabledDates and the month bounds apply to calendar days only; time is not constrained.
The trigger's accessible name includes the label and the displayed value. The popup has an accessible title, the time picker is a labelled group (timeLabel) of listboxes named by timeUnitLabels. In a time column, Arrow Up/Down move one step, Page Up/Down five, and Home/End jump to the ends; the selection follows the focus. Escape returns focus to the trigger.
Time zone and formatting
Dates and times are read and written in the device's local time zone. Convert explicitly before sending a value to a server, and avoid assuming a UTC instant. A local time that does not exist because of a daylight-saving gap resolves to the next valid local time.
The time columns use a 12-hour cycle (with an AM/PM column) or a 24-hour cycle. By default the locale's time pattern decides, and a 12-hour cycle applies without a locale; set hourCycle to override it. The trigger text follows dateFormat (default "PPP p"), so pair hourCycle={24} with a 24-hour format such as "PPP HH:mm" to keep the two in agreement.
This is not a native form input: serialize your date explicitly. The public API is restricted to the listed props, not arbitrary native input attributes. Playground Reset clears the value and closes the popup; generated code reproduces configuration rather than transient selection.
Props
| Prop | Type / Description | Default |
|---|---|---|
| motion | booleanEnables popup, calendar, and button motion; system reduced-motion takes priority. | true |
| applyLabel | stringDateTimePicker: apply button text. | "Apply" |
| cancelLabel | stringDateTimePicker: cancel button text. | "Cancel" |
| className | stringDateTimePicker: trigger class name. | undefined |
| clearLabel | stringDateTimePicker: clear button text. | "Clear" |
| clearable | booleanDateTimePicker: shows a Clear action that commits an empty value immediately. | false |
| dateFormat | stringDateTimePicker: date-fns display format for the trigger. Use a 24-hour token such as HH:mm together with hourCycle 24 for a 24-hour display. | "PPP p" |
| defaultOpen | booleanDateTimePicker: initial uncontrolled popover visibility. | false |
| defaultValue | Date | undefinedDateTimePicker: initial uncontrolled date and local time. | undefined |
| disabled | booleanDateTimePicker: disables opening the trigger and closes an open popup. | false |
| disabledDates | Matcher | Matcher[]DateTimePicker: calendar days that cannot be selected. Time is not constrained. | undefined |
| endMonth | DateDateTimePicker: latest navigable month. | undefined |
| hourCycle | 12 | 24DateTimePicker: hour cycle of the time columns. By default the locale's time pattern decides (12-hour without a locale); set it so the columns agree with dateFormat. | from locale |
| timeUnitLabels | { hours: string; minutes: string; seconds: string; period: string }DateTimePicker: accessible names of the hour, minute, second and AM/PM columns. | English labels |
| id | stringDateTimePicker: optional stable id for the label relationship. | generated |
| label | stringDateTimePicker: visible accessible name. | required |
| locale | date-fns LocaleDateTimePicker: display and calendar locale. | undefined |
| onOpenChange | (open: boolean) => voidDateTimePicker: observes popover visibility changes. | undefined |
| onValueChange | (date: Date | undefined) => voidDateTimePicker: fires on Apply with the chosen day and time, or on Clear with undefined. Never fires while editing the draft. | undefined |
| open | booleanDateTimePicker: controlled popover visibility. | uncontrolled |
| placeholder | stringDateTimePicker: trigger text when empty. | "Select date and time" |
| precision | "minute" | "second"DateTimePicker: time granularity. Emitted dates always have zero milliseconds, and zero seconds at minute precision. | "minute" |
| readOnly | booleanDateTimePicker: opens the popup but prevents changes. | false |
| startMonth | DateDateTimePicker: earliest navigable month. | undefined |
| timeLabel | stringDateTimePicker: time field label. | "Time" |
| value | Date | undefinedDateTimePicker: controlled date and local time. | uncontrolled |
DateTimePicker uses a restricted props API and does not forward arbitrary native attributes, refs or event handlers. id labels the trigger; className styles the trigger; timeLabel names the time field. Native form name, required and input submission are not supported.
Source
"use client";
import { cn } from "cn";
import { format } from "date-fns";
import type { Locale } from "date-fns";
import { CalendarClock } from "lucide-react";
import * as React from "react";
import type { Matcher } from "react-day-picker";
import { Button } from "./button";
import { Calendar } from "./calendar";
import { PickerFooter, TimeField } from "./date-time-picker-parts";
import type { TimeUnitLabels } from "./date-time-picker-parts";
import {
defaultTimeValue,
formatTimeValue,
mergeDateAndTime,
normalizeDateTime,
parseTimeValue,
resolveHourCycle,
} from "./date-time-picker-utils";
import type { DateTimePrecision, HourCycle } from "./date-time-picker-utils";
import {
Popover,
PopoverContent,
PopoverTitle,
PopoverTrigger,
} from "./popover";
// Composes Vandor's Button, Popover and Calendar with a Vandor time field. Values are
// local-time `Date`s; the draft is committed only by Apply (or Clear).
export type { DateTimePrecision, HourCycle, TimeUnitLabels };
export interface DateTimePickerProps {
id?: string;
label: string;
placeholder?: string;
clearable?: boolean;
value?: Date;
defaultValue?: Date;
onValueChange?: (date: Date | undefined) => void;
locale?: Locale;
dateFormat?: string;
precision?: DateTimePrecision;
hourCycle?: HourCycle;
timeUnitLabels?: TimeUnitLabels;
disabled?: boolean;
readOnly?: boolean;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
disabledDates?: Matcher | Matcher[];
startMonth?: Date;
endMonth?: Date;
timeLabel?: string;
applyLabel?: string;
cancelLabel?: string;
clearLabel?: string;
className?: string;
motion?: boolean;
}
const withPickerDefaults = ({
placeholder = "Select date and time",
clearable = false,
dateFormat = "PPP p",
precision = "minute",
defaultOpen = false,
timeLabel = "Time",
applyLabel = "Apply",
cancelLabel = "Cancel",
clearLabel = "Clear",
motion = true,
...props
}: DateTimePickerProps) => ({
...props,
applyLabel,
cancelLabel,
clearLabel,
clearable,
dateFormat,
defaultOpen,
motion,
placeholder,
precision,
timeLabel,
});
export const DateTimePicker = (props: DateTimePickerProps) => {
const {
id,
label,
placeholder,
clearable,
value,
defaultValue,
onValueChange,
locale,
dateFormat,
precision,
hourCycle,
timeUnitLabels,
disabled,
readOnly,
open,
defaultOpen,
onOpenChange,
disabledDates,
startMonth,
endMonth,
timeLabel,
applyLabel,
cancelLabel,
clearLabel,
className,
motion,
} = withPickerDefaults(props);
const blocked = Boolean(disabled || readOnly);
const controlled = Object.hasOwn(props, "value");
const generatedId = React.useId();
const triggerId = id ?? generatedId;
const [internal, setInternal] = React.useState(defaultValue);
const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
const committed = controlled ? value : internal;
const isOpen = !disabled && (open === undefined ? internalOpen : open);
React.useEffect(() => {
if (disabled) {
setInternalOpen(false);
}
}, [disabled]);
const fromCommitted = () => ({
day: committed,
time: committed
? formatTimeValue(committed, precision)
: defaultTimeValue("start", precision),
});
const [draft, setDraft] = React.useState(fromCommitted);
// The draft mirrors the committed value whenever the popup is closed, so a reopen
// (including a controlled `open`) never shows a discarded or stale edit.
const resetDraft = React.useEffectEvent(() => setDraft(fromCommitted()));
const committedTime = committed?.getTime();
React.useEffect(() => {
if (!isOpen) {
resetDraft();
}
}, [isOpen, committedTime, precision]);
const setOpen = (next: boolean) => {
if (disabled && next) {
return;
}
if (open === undefined) {
setInternalOpen(next);
}
onOpenChange?.(next);
};
const commit = (next?: Date) => {
if (blocked) {
return;
}
if (!controlled) {
setInternal(next);
}
onValueChange?.(next);
setOpen(false);
};
const parsedTime = parseTimeValue(draft.time);
const canApply =
!blocked && draft.day !== undefined && parsedTime !== undefined;
const apply = () => {
if (canApply && draft.day && parsedTime) {
commit(mergeDateAndTime(draft.day, parsedTime, precision));
}
};
const whileTap = motion ? { scale: 0.96 } : false;
const shown = committed
? format(normalizeDateTime(committed, precision), dateFormat, { locale })
: placeholder;
return (
<div data-slot="date-time-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
)}
>
<CalendarClock aria-hidden="true" className="size-4 shrink-0" />
<span
id={`${triggerId}-value`}
className={cn("truncate", !committed && "text-muted-foreground")}
>
{shown}
</span>
</PopoverTrigger>
<PopoverContent
align="start"
animated={motion}
className="w-auto gap-0 p-0"
>
<PopoverTitle className="sr-only">{label}</PopoverTitle>
<div className="flex flex-col sm:flex-row">
<Calendar
mode="single"
motion={motion}
autoFocus
defaultMonth={draft.day}
locale={locale}
selected={draft.day}
disabled={blocked ? () => true : disabledDates}
startMonth={startMonth}
endMonth={endMonth}
onSelect={(day) => {
// Re-clicking the selected day must not empty the draft.
if (day && !blocked) {
setDraft((current) => ({ ...current, day }));
}
}}
/>
<div className="border-t p-3 sm:border-t-0 sm:border-l">
<TimeField
id={`${triggerId}-time`}
label={timeLabel}
value={draft.time}
precision={precision}
hourCycle={resolveHourCycle(locale, hourCycle)}
locale={locale}
unitLabels={timeUnitLabels}
disabled={disabled}
readOnly={readOnly}
onEnter={apply}
onValueChange={(time) =>
setDraft((current) => ({ ...current, time }))
}
/>
</div>
</div>
<div className="border-t p-3">
<PickerFooter
showClear={Boolean(clearable && committed && !blocked)}
canApply={canApply}
whileTap={whileTap}
labels={{
apply: applyLabel,
cancel: cancelLabel,
clear: clearLabel,
}}
onClear={() => commit()}
onCancel={() => setOpen(false)}
onApply={apply}
/>
</div>
</PopoverContent>
</Popover>
</div>
);
};
Credits
- shadcn/ui: Button, Popover, and Calendar composition pattern; Vandor adds the time columns, the Apply/Cancel draft flow, precision control, and motion controls.