# Date Time Picker

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

> 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 { 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 [#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-time-picker.json
```

## Dependencies [#dependencies]

Installed automatically by the registry CLI. For manual setup:

```bash
npm install 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 [#usage]

```tsx
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](/docs/components/date-time) component, which only displays an instant. DateTimePicker follows the [Date Picker](/docs/components/date-picker) conventions but commits transactionally, like the [Date Range Picker](/docs/components/date-range-picker). `motion={false}` disables popup, Calendar, and button animations; system reduced-motion preferences take priority.

## Consumer setup [#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) [#storybook-stories-optional]

Install DateTimePicker first, then:

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

This controlled picker offers minute precision and a clear action.

<ComponentPreview name="date-time-picker-demo" title="date-time-picker-demo.tsx">
  <DateTimePickerDemo />
</ComponentPreview>

Set `precision="second"` to add a seconds column. Match `dateFormat` so the trigger shows them too.

<ComponentPreview name="date-time-picker-seconds-demo" title="date-time-picker-seconds-demo.tsx">
  <DateTimePickerSecondsDemo />
</ComponentPreview>

## Behavior & Accessibility [#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 [#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 [#props]

| Prop           | Type                                                                 | Default                | Description                                                                                                                                                           |
| -------------- | -------------------------------------------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| motion         | boolean                                                              | true                   | Enables popup, calendar, and button motion; system reduced-motion takes priority.                                                                                     |
| applyLabel     | string                                                               | "Apply"                | DateTimePicker: apply button text.                                                                                                                                    |
| cancelLabel    | string                                                               | "Cancel"               | DateTimePicker: cancel button text.                                                                                                                                   |
| className      | string                                                               | undefined              | DateTimePicker: trigger class name.                                                                                                                                   |
| clearLabel     | string                                                               | "Clear"                | DateTimePicker: clear button text.                                                                                                                                    |
| clearable      | boolean                                                              | false                  | DateTimePicker: shows a Clear action that commits an empty value immediately.                                                                                         |
| dateFormat     | string                                                               | "PPP p"                | 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.                          |
| defaultOpen    | boolean                                                              | false                  | DateTimePicker: initial uncontrolled popover visibility.                                                                                                              |
| defaultValue   | Date \| undefined                                                    | undefined              | DateTimePicker: initial uncontrolled date and local time.                                                                                                             |
| disabled       | boolean                                                              | false                  | DateTimePicker: disables opening the trigger and closes an open popup.                                                                                                |
| disabledDates  | Matcher \| Matcher\[]                                                | undefined              | DateTimePicker: calendar days that cannot be selected. Time is not constrained.                                                                                       |
| endMonth       | Date                                                                 | undefined              | DateTimePicker: latest navigable month.                                                                                                                               |
| hourCycle      | 12 \| 24                                                             | from locale            | 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. |
| timeUnitLabels | \{ hours: string; minutes: string; seconds: string; period: string } | English labels         | DateTimePicker: accessible names of the hour, minute, second and AM/PM columns.                                                                                       |
| id             | string                                                               | generated              | DateTimePicker: optional stable id for the label relationship.                                                                                                        |
| label          | string                                                               | required               | DateTimePicker: visible accessible name.                                                                                                                              |
| locale         | date-fns Locale                                                      | undefined              | DateTimePicker: display and calendar locale.                                                                                                                          |
| onOpenChange   | (open: boolean) => void                                              | undefined              | DateTimePicker: observes popover visibility changes.                                                                                                                  |
| onValueChange  | (date: Date \| undefined) => void                                    | undefined              | DateTimePicker: fires on Apply with the chosen day and time, or on Clear with undefined. Never fires while editing the draft.                                         |
| open           | boolean                                                              | uncontrolled           | DateTimePicker: controlled popover visibility.                                                                                                                        |
| placeholder    | string                                                               | "Select date and time" | DateTimePicker: trigger text when empty.                                                                                                                              |
| precision      | "minute" \| "second"                                                 | "minute"               | DateTimePicker: time granularity. Emitted dates always have zero milliseconds, and zero seconds at minute precision.                                                  |
| readOnly       | boolean                                                              | false                  | DateTimePicker: opens the popup but prevents changes.                                                                                                                 |
| startMonth     | Date                                                                 | undefined              | DateTimePicker: earliest navigable month.                                                                                                                             |
| timeLabel      | string                                                               | "Time"                 | DateTimePicker: time field label.                                                                                                                                     |
| value          | Date \| undefined                                                    | uncontrolled           | DateTimePicker: controlled date and local time.                                                                                                                       |

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

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

* [shadcn/ui](https://ui.shadcn.com/docs/components/base/date-picker): Button, Popover, and Calendar composition pattern; Vandor adds the time columns, the Apply/Cancel draft flow, precision control, and motion controls.
