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 Time Picker

A transactional date and time picker combining a calendar and a time picker in an accessible popover.

Playground

Live preview
Choose a day and a time as a draft, then Apply. Cancel, Escape, and outside dismissal discard changes; Clear commits immediately.
Customize date-time-picker
Customize
date-time-picker-demo.tsx
"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:

Dependencies4 required packages
  • 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.2

Copying 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.

date-time-picker-demo.tsx
"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.

date-time-picker-seconds-demo.tsx
"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

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

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

true
applyLabelstring

DateTimePicker: apply button text.

"Apply"
cancelLabelstring

DateTimePicker: cancel button text.

"Cancel"
classNamestring

DateTimePicker: trigger class name.

undefined
clearLabelstring

DateTimePicker: clear button text.

"Clear"
clearableboolean

DateTimePicker: shows a Clear action that commits an empty value immediately.

false
dateFormatstring

DateTimePicker: 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"
defaultOpenboolean

DateTimePicker: initial uncontrolled popover visibility.

false
defaultValueDate | undefined

DateTimePicker: initial uncontrolled date and local time.

undefined
disabledboolean

DateTimePicker: disables opening the trigger and closes an open popup.

false
disabledDatesMatcher | Matcher[]

DateTimePicker: calendar days that cannot be selected. Time is not constrained.

undefined
endMonthDate

DateTimePicker: latest navigable month.

undefined
hourCycle12 | 24

DateTimePicker: 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
idstring

DateTimePicker: optional stable id for the label relationship.

generated
labelstring

DateTimePicker: visible accessible name.

required
localedate-fns Locale

DateTimePicker: display and calendar locale.

undefined
onOpenChange(open: boolean) => void

DateTimePicker: observes popover visibility changes.

undefined
onValueChange(date: Date | undefined) => void

DateTimePicker: fires on Apply with the chosen day and time, or on Clear with undefined. Never fires while editing the draft.

undefined
openboolean

DateTimePicker: controlled popover visibility.

uncontrolled
placeholderstring

DateTimePicker: 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"
readOnlyboolean

DateTimePicker: opens the popup but prevents changes.

false
startMonthDate

DateTimePicker: earliest navigable month.

undefined
timeLabelstring

DateTimePicker: time field label.

"Time"
valueDate | undefined

DateTimePicker: 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.