Skip to content

Datetime picker

Picks a date and a time, typed or chosen from a calendar and a list. Holds a local date and time, like a Date/Time field.

Start

Installation

pnpm dlx shadcn@latest add @wisty/datetime-picker

The CLI installs the Radix or Base UI file to match your project, with what it builds on: date-picker, time-picker, field.

If your project doesn't have Wisty UI's theme yet, add it once with npx shadcn@latest add @wisty/cosmos. Until then, components keep shadcn's default look. Installation has the details.

When to use

Use it

  • People enter a date and a time in one field, like an event's start, a callback or an appointment.
  • The value is a Salesforce Date/Time field, set by a person reading their own clock. The datetime picker holds it as a local date and time, "YYYY-MM-DDTHH:MM", and the app turns it into an instant in its time zone.
  • People type each half or pick it: a date from the calendar, a time from the list.
  • Only a window is allowed, like the next three business days. Pass min and max as dates and times.
  • It replaces lightning-input with type="datetime".

Don't use it

  • The value is a day, like a close date. Use Date picker.
  • The value is a time of day with no date, like a store's opening time. Use Time picker.
  • People pick a range, like an event's start and end. Use two datetime pickers, each bounding the other with min and max.
  • The date and time are only read. Show them with Output field, which formats an instant in a time zone.

Usage

import { FieldLegend, FieldSet } from "@/components/ui/field"
import {
  DateTimePicker,
  DateTimePickerDate,
  DateTimePickerTime,
} from "@/components/wisty/datetime-picker"
DateTimePicker
├── DateTimePickerDate
└── DateTimePickerTime

The field's label is a FieldLegend in a FieldSet. Pass its id as the picker's aria-labelledby, and the inputs are named "Start Date" and "Start Time" after it and their own labels:

<FieldSet>
  <FieldLegend variant="label" id="start-label">
    Start
  </FieldLegend>
  <DateTimePicker aria-labelledby="start-label">
    <DateTimePickerDate />
    <DateTimePickerTime />
  </DateTimePicker>
</FieldSet>

onValueChange gets the date and time once both halves hold one the field accepts. Until then it gets null, and its details name the half that's missing, or the half that's invalid with the error. In a form, name submits one value, "YYYY-MM-DDTHH:MM".

The value has no time zone. Turn it into an instant, the shape Salesforce stores and Output field shows, with the zone the person is in, and back:

import {
  dateTimeToInstant,
  instantToDateTime,
} from "@/components/wisty/datetime-picker"
const start = instantToDateTime("2026-06-18T13:00:00.000Z", {
  timeZone: "America/New_York",
})
// "2026-06-18T09:00"

const startDateTime = dateTimeToInstant(start, {
  timeZone: "America/New_York",
})
// "2026-06-18T13:00:00.000Z"

Examples

Edit a saved value

A saved start shown in both halves, controlled with `value` and `onValueChange`, and submitted as one value with `name`.

Start

Min and max

A callback window with `min` and `max`: days outside are disabled, and on the first and last day the time list starts and stops at their times.

Callback

From 9:00 AM today to 5:00 PM on Friday.

States

A required field showing the error on the half `onValueChange` names, a `disabled` one and a `readOnly` one.

*Start
Created
End

In a dialog

An event's start in a modal dialog: Escape closes the calendar or the time list first, then the dialog.

API reference

PartBuilt on
DateTimePicker<div> (opens in a new tab)
DateTimePickerDateDatePickerInput
DateTimePickerTimeTimePickerInput

DateTimePicker

PropTypeDefaultDescription
valuestring | nullA local date and time, "YYYY-MM-DDTHH:MM", never an instant; null when a half is empty or invalid.
defaultValuestring | nullnull
onValueChange( value: string | null, details: DateTimePickerChangeDetails ) => voidCalled when either half changes; the value is null until both are accepted, details say which isn't.
minstringThe earliest date and time, as "YYYY-MM-DDTHH:MM".
maxstringThe latest date and time, as "YYYY-MM-DDTHH:MM".
localestring"en-US"Formats and reads both halves.
dateStyle"short" | "medium" | "long""medium"How the date shows, as the date picker's.
timeStyle"short" | "medium""short"How the time shows, as the time picker's: "medium" adds seconds.
stepnumberMinutes between the times in the time list.
todaystringThe day the calendar marks as today, as "YYYY-MM-DD".
namestringSubmits the value with a form, from a hidden input.
requiredbooleanfalse
disabledbooleanfalse
readOnlybooleanfalse
dateLabelReact.ReactNode"Date"The date input's label, shown above it.
timeLabelReact.ReactNode"Time"The time input's label, shown above it.

instantToDateTimefunction

An instant as a local date and time on a time zone's clock, "YYYY-MM-DDTHH:MM" (with seconds, "YYYY-MM-DDTHH:MM:SS"). Null for something that isn't an instant.

ParameterTypeDefault
instant*string | number | Date
__1*{ timeZone: string; seconds?: boolean }

Returns string | null.

dateTimeToInstantfunction

A local date and time on a time zone's clock as an instant, an ISO string in UTC. A time the clock shows twice is the first; a time it skips moves forward by the gap. Null for a value that isn't a real date and time, like "2026-02-30T10:00".

ParameterTypeDefault
value*string
__1*{ timeZone: string }

Returns string | null.

DateTimePickerErrortype

type DateTimePickerError = "format" | "min" | "max"

DateTimePickerParttype

type DateTimePickerPart = "date" | "time"

DateTimePickerChangeDetailstype

MemberTypeDefaultDescription
date*string | nullThe date half, as "YYYY-MM-DD", or null when it's empty or invalid.
time*string | nullThe time half, as "HH:MM" or "HH:MM:SS", or null when it's empty or invalid.
missingDateTimePickerPartThe empty half, when the other holds a value.
errorDateTimePickerErrorWhy a half isn't accepted: unreadable text, or a date and time outside min or max.
invalidDateTimePickerPartThe half the error is in.

SLDS mapping

The datetime picker is SLDS's datetime picker as lightning-input draws it: a compound form element (slds-form-element_compound), whose legend is the page's FieldSet and FieldLegend, and a row of two form elements (slds-form-element__row). DateTimePickerDate is the date picker's form element with its label, and DateTimePickerTime the timepicker's (slds-timepicker).

Known gaps:

  • SLDS swaps in the phone's own date and time input on mobile. The datetime picker keeps its two fields.
  • The halves sit side by side at every width; SLDS's small container wraps them.
  • The labels the halves add ("Date", "Time") are English unless you pass dateLabel and timeLabel.
PropValueSLDSNotes
(no value)default (added)slds-datetime-picker, slds-form-element_compound, slds-form-element__row, slds-datepicker, slds-timepickerOne field with a legend, and the date picker and the time picker side by side, each with its own label.
dateStyleshort, medium, long (added)lightning-input date-styleHow the date half shows the day, as the date picker's. Medium by default.
timeStyleshort, medium (added)lightning-input time-styleHow the time half shows the time, as the time picker's. Medium adds seconds.
min, max(a date and time) (added)slds-has-errorDays outside are disabled in the calendar; on min's and max's days the time list starts or stops at their times. An error shows on the half that's out of range.
aria-invalidtrue (added)slds-has-errorOn DateTimePickerDate or DateTimePickerTime, for an error the caller decides; the halves set it for text they can't read and values out of range.
disabledtrue (added)slds-input[disabled]
readOnlytrue (added)slds-input[readonly]

Keyboard interactions

Keys Action
Tab, Shift+Tab Move from the date field to the calendar button to the time field.
Down Arrow In the date field, opens the calendar. In the time field, opens the list and moves through the times.
Enter In the date field, reads the typed date. In the time field, picks the highlighted time or reads the typed one.
Escape Closes the calendar or the time list. Inside a modal dialog, the first Escape closes only them.

The calendar and the time list keep the date picker's and the time picker's other keys.

Accessibility

  • Pass aria-labelledby with the legend's id, or aria-label with the field's label, so each input's name starts with the field's.
  • For an error you decide, like an empty required field, set aria-invalid and aria-describedby on DateTimePickerDate or DateTimePickerTime, the half it's about, or both. The halves set aria-invalid themselves for text they can't read and values out of range.
  • In Cosmos dark the error text is about 4.3:1, below WCAG AA's 4.5:1. SLDS's own stylesheet does the same.

Rules

  • Keep the value a local "YYYY-MM-DDTHH:MM" string. Never pass an instant ("…Z", a Date, a timestamp): convert it with instantToDateTime in the time zone the person reads their clock in, and convert back with dateTimeToInstant in the same zone.
  • Put DateTimePickerDate before DateTimePickerTime, so the tab order runs date, calendar, time.
  • A time the clock skips when it goes forward becomes the time after the gap, and a time it shows twice when it goes back is the first. Say so where it matters, like a 2:30 AM appointment.
  • Disabled, the datetime picker submits nothing with a form, as a disabled input doesn't.
  • Never pass asChild or render.