Siteplane UI

Date Time Picker

Edit a local date and time in one control.

Markdown

The post goes live at this time in your local time zone.

"use client";

import { DateTimePicker } from "@/siteplane/components/date-time-picker";
import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@/siteplane/components/field";

export default function Example() {
  return (
    <Field className="w-full max-w-sm">
      <FieldLabel>Publish at</FieldLabel>
      <DateTimePicker
        className="w-full"
        defaultValue={{
          date: new Date(2026, 9, 6),
          time: { hour: 9, minute: 0 },
        }}
      />
      <FieldDescription>
        The post goes live at this time in your local time zone.
      </FieldDescription>
    </Field>
  );
}

Installation

pnpm dlx shadcn@4.18.0 add @siteplane/date-time-picker

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

pnpm dlx shadcn@4.18.0 add @siteplane/button @siteplane/form @siteplane/input

Field is installed with Input.

Usage

import { DateTimePicker } from "@/siteplane/components/date-time-picker";

<DateTimePicker aria-label="Meeting start" />;

Controlled value

Pass value and onValueChange to keep the date and time in React state. onValueChange receives undefined when the field is cleared; store null for an empty controlled value, because undefined makes the picker uncontrolled.

"use client";

import {
  DateTimePicker,
  type DateTimePickerValue,
} from "@/siteplane/components/date-time-picker";
import { useState } from "react";

export default function Example() {
  const [value, setValue] = useState<DateTimePickerValue | null>({
    date: new Date(2026, 8, 7),
    time: { hour: 9, minute: 30 },
  });
  return (
    <DateTimePicker
      aria-label="Meeting start"
      value={value}
      onValueChange={(next) => setValue(next ?? null)}
    />
  );
}

12-hour clock

Set hourCycle={12} for an application that shows times such as 9:00 PM. The time adds an AM/PM segment after the minutes, which takes focus and arrow keys like the other segments; typing A or P sets it. The time selector names each hour with its day period, such as 09 PM. The value stays in 24-hour time, and the form still receives YYYY-MM-DDTHH:mm.

import { DateTimePicker } from "@/siteplane/components/date-time-picker";

<DateTimePicker aria-label="Snooze until" hourCycle={12} />;

Unavailable dates

Use calendarProps.disabled to block past dates and weekends. The calendar doesn't offer these dates, and a typed date that breaks a rule marks the field invalid once it loses focus or the form is submitted; FieldError shows the message. Time rules, such as business hours, are not checked, so validate them in your form and again on the server.

Interviews take place Monday to Friday.

"use client";

import { DateTimePicker } from "@/siteplane/components/date-time-picker";
import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
} from "@/siteplane/components/field";
import { useEffect, useState } from "react";

export default function Example() {
  // Read today in the browser once the example mounts. A page rendered on a
  // server or at build time would otherwise keep the day it was rendered.
  const [today, setToday] = useState<Date>();
  useEffect(() => setToday(new Date()), []);
  return (
    <Field className="w-full max-w-sm">
      <FieldLabel>Interview</FieldLabel>
      <DateTimePicker
        calendarProps={{
          disabled: [{ before: today }, { dayOfWeek: [0, 6] }],
        }}
        className="w-full"
      />
      <FieldDescription>
        Interviews take place Monday to Friday.
      </FieldDescription>
      <FieldError />
    </Field>
  );
}

Minute step

minuteStep sets the minute options in the time selector, here every 15 minutes. Typed minutes are not rounded, so check the step on the server if it matters.

"use client";

import { DateTimePicker } from "@/siteplane/components/date-time-picker";
import { Field, FieldLabel } from "@/siteplane/components/field";

export default function Example() {
  return (
    <Field className="w-full max-w-sm">
      <FieldLabel>Meeting start</FieldLabel>
      <DateTimePicker
        className="w-full"
        defaultValue={{
          date: new Date(2026, 9, 14),
          time: { hour: 10, minute: 30 },
        }}
        minuteStep={15}
      />
    </Field>
  );
}

Disabled, read-only and invalid

A disabled Field also disables the picker, and readOnly shows a date and time that can't be changed on the regular field surface. For an error from your application, such as a time outside business hours, set invalid on Field and explain the problem in a FieldError.

Set by the organizer.

Choose a time between 09:00 and 17:00.
"use client";

import { DateTimePicker } from "@/siteplane/components/date-time-picker";
import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
} from "@/siteplane/components/field";

export default function Example() {
  return (
    <div className="grid w-full max-w-sm gap-4">
      <Field disabled>
        <FieldLabel>Meeting start</FieldLabel>
        <DateTimePicker
          className="w-full"
          defaultValue={{
            date: new Date(2026, 9, 14),
            time: { hour: 10, minute: 30 },
          }}
        />
      </Field>
      <Field>
        <FieldLabel>Webinar start</FieldLabel>
        <DateTimePicker
          className="w-full"
          defaultValue={{
            date: new Date(2026, 9, 21),
            time: { hour: 16, minute: 0 },
          }}
          readOnly
        />
        <FieldDescription>Set by the organizer.</FieldDescription>
      </Field>
      <Field invalid>
        <FieldLabel>Call start</FieldLabel>
        <DateTimePicker
          className="w-full"
          defaultValue={{
            date: new Date(2026, 9, 16),
            time: { hour: 19, minute: 30 },
          }}
        />
        <FieldError match={true}>
          Choose a time between 09:00 and 17:00.
        </FieldError>
      </Field>
    </div>
  );
}

Submission and reset

With name, DateTimePicker submits a hidden local YYYY-MM-DDTHH:mm string without seconds, UTC offset or Z suffix; convert time zones in your application. The example submits { "startsAt": "2026-10-14T10:30" }, and an empty field submits an empty string. Changing locale changes only the display. With required, submitting an empty field is blocked, the field is focused and FieldError shows the required message. Native form reset restores defaultValue; reset controlled state yourself.

"use client";

import { Button } from "@/siteplane/components/button";
import { DateTimePicker } from "@/siteplane/components/date-time-picker";
import { Field, FieldError, FieldLabel } from "@/siteplane/components/field";
import { Form } from "@/siteplane/components/form";

export default function Example() {
  return (
    <Form
      className="grid w-full max-w-sm gap-4"
      onSubmit={(event) => {
        event.preventDefault();
      }}
    >
      <Field>
        <FieldLabel>Starts at</FieldLabel>
        <DateTimePicker
          className="w-full"
          defaultValue={{
            date: new Date(2026, 9, 14),
            time: { hour: 10, minute: 30 },
          }}
          name="startsAt"
          required
        />
        <FieldError />
      </Field>
      <div className="flex gap-2">
        <Button type="submit">Submit</Button>
        <Button type="reset" variant="outline">
          Reset
        </Button>
      </div>
    </Form>
  );
}

API reference

Component properties
PropTypeDefaultDescription
aria-labelstringunsetAccessible name when no FieldLabel or aria-labelledby names the picker.
aria-labelledby / aria-describedbystringunsetExternal label and help ids; inside a Field, FieldLabel, FieldDescription and FieldError are connected automatically.
aria-invalidAriaAttributes[aria-invalid]unsetExternal invalid state; while set, it replaces the built-in date checks.
value / defaultValueDateTimePickerValue | null (value only)uncontrolled / unsetControlled or initial value; null is an empty controlled value.
onValueChange(value: DateTimePickerValue | undefined) => voidunsetReports a complete date and time, or undefined when cleared.
disabledbooleanfalsePrevents editing/opening; a disabled Field also disables the picker.
classNamestringunsetOuter control styling; the picker keeps its content width unless a width utility such as w-full is set.
localestringcontextFormatting locale, not time zone.
calendarPropsCalendarOptionsunsetThe calendar options below: defaultMonth, disabled, startMonth, endMonth and todayDate.
readOnly / requiredbooleanfalseRead-only field on the regular surface; required blocks form submission while empty, focuses the field and shows the required message in FieldError.
sizesm | md | lg | xlprovider / lgControl size.
textSizesm | baseunsetField text size; unset is 14px, or 16px at xl, like Input.
variantdefault | elevated | inset | softdefaultInput surface.
formstringunsetAssociated form ID.
namestringunsetHidden serialized input name; a Field name takes precedence.
minuteStepnumber5Minute selector options; normalized to an integer between 1 and 60.
hourCycle12 | 2424Clock of the time segments and the time selector; 12 adds the locale's AM/PM segment. The value stays in 24-hour time.
openDatePickerLabel / openTimePickerLabelstringOpen date calendar / Open time selectorPicker button labels.
incompleteLabelstringEnter a complete date and time.The error of a required picker while its date and time are incomplete, shown by FieldError.

size and variant use the same scale and surfaces as Input; see Input sizes and variants.

Calendar options

calendarProps takes CalendarOptions, the type of the same options on Calendar. startMonth and endMonth (Date, unset) limit the calendar to the first and last allowed months. disabled (unset) makes days unavailable with one matcher or a list of matchers, each a Date, a (date: Date) => boolean test or a rule with after, before, from, to and dayOfWeek (0 Sunday through 6 Saturday); true disables the whole calendar. Both apply to picking in the calendar and to typed dates. defaultMonth (Date, unset) sets the month the calendar shows when it opens; while it is set, it also wins over the month of the selected date. todayDate (Date, the device's date) sets the day the calendar marks as today; without a selected date, the calendar opens on its month, and typed segments start from it.

Exported types: DateTimePickerProps, DateTimePickerValue. These describe the contracts above.

Read the installed source.

Usage notes

Values use native Date calendar fields in the local runtime time zone. Locale changes formatting, not the zone. DateTimePickerValue is {date:Date,time:{hour,minute}}; no time-zone selector or automatic zone conversion is exposed. The time button stays disabled until a date is set, and the time selector's labels (Hours, Minutes) are English. Inside a Field, FieldLabel names DateTimePicker and clicking it focuses the first segment; outside a Field, pass aria-label or aria-labelledby. Arrow keys change the focused date or time segment. Only the props listed above are supported, and DateTimePicker renders its own segments, buttons and popovers; it accepts no children or ref. The named Props type and value type are exported from the component module.

The date and the time, with the date button between them, read in the direction of the locale, which the arrow keys follow too, and are isolated from the text around them. An en-US value keeps its date before its time in a right-to-left page, while the field keeps the page's direction: its time button sits at the page's inline end.