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.
Installation
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
minandmaxas dates and times. - It replaces
lightning-inputwithtype="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
minandmax. - 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
└── DateTimePickerTimeThe 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`.
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.
States
A required field showing the error on the half `onValueChange` names, a `disabled` one and a `readOnly` one.
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
| Part | Built on |
|---|---|
DateTimePicker | <div> (opens in a new tab) |
DateTimePickerDate | DatePickerInput |
DateTimePickerTime | TimePickerInput |
DateTimePicker
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | null | A local date and time, "YYYY-MM-DDTHH:MM", never an instant; null when a half is empty or invalid. | |
defaultValue | string | null | null | |
onValueChange | ( value: string | null, details: DateTimePickerChangeDetails ) => void | Called when either half changes; the value is null until both are accepted, details say which isn't. | |
min | string | The earliest date and time, as "YYYY-MM-DDTHH:MM". | |
max | string | The latest date and time, as "YYYY-MM-DDTHH:MM". | |
locale | string | "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. |
step | number | Minutes between the times in the time list. | |
today | string | The day the calendar marks as today, as "YYYY-MM-DD". | |
name | string | Submits the value with a form, from a hidden input. | |
required | boolean | false | |
disabled | boolean | false | |
readOnly | boolean | false | |
dateLabel | React.ReactNode | "Date" | The date input's label, shown above it. |
timeLabel | React.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.
| Parameter | Type | Default |
|---|---|---|
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".
| Parameter | Type | Default |
|---|---|---|
value* | string | |
__1* | { timeZone: string } |
Returns string | null.
DateTimePickerErrortype
type DateTimePickerError = "format" | "min" | "max"DateTimePickerParttype
type DateTimePickerPart = "date" | "time"DateTimePickerChangeDetailstype
| Member | Type | Default | Description |
|---|---|---|---|
date* | string | null | The date half, as "YYYY-MM-DD", or null when it's empty or invalid. | |
time* | string | null | The time half, as "HH:MM" or "HH:MM:SS", or null when it's empty or invalid. | |
missing | DateTimePickerPart | The empty half, when the other holds a value. | |
error | DateTimePickerError | Why a half isn't accepted: unreadable text, or a date and time outside min or max. | |
invalid | DateTimePickerPart | The 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
dateLabelandtimeLabel.
| Prop | Value | SLDS | Notes |
|---|---|---|---|
(no value) | default (added) | slds-datetime-picker, slds-form-element_compound, slds-form-element__row, slds-datepicker, slds-timepicker | One field with a legend, and the date picker and the time picker side by side, each with its own label. |
dateStyle | short, medium, long (added) | lightning-input date-style | How the date half shows the day, as the date picker's. Medium by default. |
timeStyle | short, medium (added) | lightning-input time-style | How the time half shows the time, as the time picker's. Medium adds seconds. |
min, max | (a date and time) (added) | slds-has-error | Days 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-invalid | true (added) | slds-has-error | On DateTimePickerDate or DateTimePickerTime, for an error the caller decides; the halves set it for text they can't read and values out of range. |
disabled | true (added) | slds-input[disabled] | |
readOnly | true (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-labelledbywith the legend'sid, oraria-labelwith 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-invalidandaria-describedbyonDateTimePickerDateorDateTimePickerTime, the half it's about, or both. The halves setaria-invalidthemselves 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", aDate, a timestamp): convert it withinstantToDateTimein the time zone the person reads their clock in, and convert back withdateTimeToInstantin the same zone. - Put
DateTimePickerDatebeforeDateTimePickerTime, 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
asChildorrender.
Related
- Date pickerPicks a day, typed in the field or chosen from a calendar under it.
- Time pickerPicks a time of day, typed in the field or chosen from a list of times.
- FieldLays out a form control with its label, help text and error.
- Output fieldShows a field's label and formatted value read-only, with an edit pencil.