Date Time Picker React component for Tailwind CSS v4
v1.0.1Updated in 1.7.0
A React date time picker: a calendar beside a column of free time slots, presets and a time zone label in one field, a sheet on phones.
Click the field (or press Enter, Space or ArrowDown on it) to open a panel under it: an optional row of preset chips (In 1 hour, Tomorrow 9:00, Monday 9:00 or your own), the calendar and, beside it, a column of time slots for the chosen day. Clicking a day shows its times and keeps the time you had if that slot is free that day; otherwise the first free slot is highlighted and the field shows the day with --:-- until you click a time. Clicking a time sets the value at once and the panel stays open until Done, Enter, Escape or a click outside; the selected slot's fill glides between slots of the same day. Taken slots are struck through and can not be picked, slots from getTimeSlots can carry a note ("2 locuri"), a promise shows pulsing placeholders, and a failed request shows a retry button. A preset sets day and time together and closes. The footer shows the time zone (a select when timeZones is set: switching it keeps the moment and moves the wall clock) and Done; the inline panel shows a summary of the pick there instead. Keyboard: focus lands on the selected day (else today); arrows move by day and week, PageUp / PageDown by month, Enter picks the day; Tab moves to the time list, where the arrows move one slot, Home / End jump to the ends, PageUp / PageDown move four slots, typing digits jumps ("14" to 14:00, "9" to 09:00), Space picks and Enter picks and closes; Tab reaches the zone select and Done; Escape closes and returns focus to the field. On a phone the field opens a bottom sheet instead: presets and times are horizontal chip rails with 44px chips (the chosen time scrolls to the center), the calendar is full width, and the choice is a draft that the big Done button commits; dragging the sheet away keeps the old value. Below 640px the inline panel (and a popover with responsive off) puts the times in a grid under the calendar instead of a column beside it. Clear (clearable) empties the field.
Date Time Picker is a Pro React component in the React input components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add date-time-picker, together with Button, Calendar, Drawer, Field, Select and 8 more and the npm packages @radix-ui/react-popover, date-fns, motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add date-time-pickerDate Time Picker is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import DateTimePicker from @/components/ui/date-time-picker.
import { useState } from "react"
import { DateTimePicker, buildTimeSlots, type DateTimeSlot } from "@/components/ui/date-time-picker"
// Liam Walker's free slots: weekdays 09:00 to 17:00, 10:00 and 13:30 taken
function freeSlots(day: Date): DateTimeSlot[] {
if (day.getDay() === 0 || day.getDay() === 6) return []
return buildTimeSlots({ step: 30, start: "09:00", end: "17:00" }).map((time) => ({
time,
disabled: time === "10:00" || time === "13:30",
}))
}
export function ScheduleDemo() {
const [value, setValue] = useState<Date | null>(null)
return (
<DateTimePicker
label="Date and time"
placeholder="Pick a free slot"
value={value}
onValueChange={setValue}
getTimeSlots={freeSlots}
disablePast
presets
timeZone="America/New_York"
locale="en-US"
name="appointment"
labels={{ noSlots: "No free slots that day", tomorrowMorning: "Tomorrow, 9 AM" }}
/>
)
}Props
labeldefault""ReactNodeThe field label. Plain text also titles the phone sheet.
descriptiondefault""ReactNodeHelper text under the field, linked with aria-describedby.
errordefault""ReactNodeReplaces the description with an error and marks the field invalid.
requireddefaultfalsebooleanThe required mark after the label and aria-required on the trigger.
invaliddefaultfalsebooleanThe invalid look and aria-invalid without a message.
disableddefaultfalsebooleanDims the field and never opens the panel; the hidden input is not submitted.
readOnlydefaultfalsebooleanShows the value but never opens the panel (inline: picks are ignored).
valueDate | nullThe moment, controlled (an absolute Date; null is empty). A value off the step grid (16:37) shows as is and as an extra slot in the list.
defaultValuedefault"null"Date | nullThe starting moment while uncontrolled. A form reset returns to it.
onValueChange(value: Date | null) => voidCalled when a slot or a preset is picked, when a new day keeps the chosen time, on Clear, and on the sheet's Done.
minuteStepdefault15numberMinutes between slots when getTimeSlots is not set; also rounds the In 1 hour preset.
startTimedefault"00:00"stringThe first slot of the day, "HH:mm" ("08:00" for business hours).
endTimedefault"23:59"stringThe last possible slot of the day, "HH:mm".
getTimeSlots(day: Date) => a list of "HH:mm" strings or DateTimeSlot objects, or a Promise of that listAvailability per day (a local midnight Date for the wall day in the zone), replacing the step list. Items are "HH:mm" or { time, disabled?, label? }. A promise shows placeholder slots, a rejection shows noSlots with a retry. Answers are cached per function and day, so pass a stable function.
disabledTimes(slot: Date) => booleanDisables single slots by their absolute moment.
minDateDays and slots before are disabled (the day of min stays open for its later slots).
maxDateDays and slots after are disabled. Ignored when it is before min.
disablePastdefaultfalsebooleanShorthand for min = now: past days and today's past slots are disabled.
disabledDaysDateMatcher or DateMatcher[] (DateMatcher: Date, Date[], { from, to }, { before }, { after }, { dayOfWeek } or (date) => boolean)Days the calendar disables: a date, a list, { from, to }, { before }, { after }, { dayOfWeek: [0, 6] } or a test.
presetsdefaultfalseboolean | DateTimePreset[]true: In 1 hour (rounded up to the step), Tomorrow 9:00 and Monday 9:00. Or your own { label, value, key? } list. A preset sets day and time and closes the panel.
timeZonedefault""stringThe IANA zone the wall times are picked in ("America/New_York"); the calendar and slots show its clock and DST gaps are disabled. Empty uses the device zone; an unknown zone falls back to it with a warning.
showTimeZonedefaulttruebooleanThe zone label in the footer.
timeZonesstring[]Makes the zone label a select to switch zones; the moment stays, the wall clock moves.
onTimeZoneChange(zone: string) => voidCalled when a zone is picked from the select.
hourCycledefault"auto""auto" | "h12" | "h23"12 or 24 hour slots and trigger text; auto follows the locale.
displayFormatdefault"{ weekday: "short", day: "numeric", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" }"Intl.DateTimeFormatOptionsHow the trigger writes the value ("Thu, Sep 24, 2026, 4:40 PM"). While only a day is picked its date part is shown with --:--.
placeholderdefault"Pick date and time"stringThe trigger text while empty.
clearabledefaultfalsebooleanA clear button in the field while it has a value (and Clear in the sheet).
inlinedefaultfalsebooleanRenders the panel in the page without a trigger.
weekStartsOndefault-10 | 1 | 2 | 3 | 4 | 5 | 6 | -1The calendar's first weekday (0 Sunday to 6 Saturday); -1 uses the locale's.
calendarPropsPartial<CalendarProps>Anything else for the calendar: size, radius, events, showWeekNumbers, labels.
nowDate | numberWhat now means for presets and disablePast. Default: the clock, ticking each minute (null until hydrated).
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The selected day, slot and preset, and the Done button.
colordefault""stringAny CSS color; replaces the tone.
iconReactNodeThe trigger's leading icon. Default: CalendarClock.
sizedefault"md""sm" | "md" | "lg"The field box: 32 / 40 / 48px (md grows to 44px on touch).
variantdefault"soft""soft" | "outline" | "underline" | "ghost"The field box look.
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"The field box corners.
namedefault""stringSubmits value.toISOString() through a hidden input.
defaultOpendefaultfalsebooleanStarts with the panel (or the sheet) open.
openbooleanWhether the panel is open, controlled. It never opens while disabled or readOnly.
onOpenChange(open: boolean) => voidCalled when the panel opens or closes.
responsivedefaulttruebooleanBelow 768px the panel opens as a bottom sheet with chip rails and a draft committed by Done.
sheetTitledefault""stringThe sheet title; empty uses the label's text, else labels.dialog.
sidedefault"bottom""top" | "right" | "bottom" | "left"Where the popover opens (it flips when there is no room).
aligndefault"start""start" | "center" | "end"The popover's alignment to the field.
containerHTMLElement | nullWhere the popover or sheet portals to.
localedefault""stringMonth and weekday names, the first weekday, the hour cycle and the zone names. Empty uses the nearest LocaleProvider or UIProvider, else the page's language.
labelsPartial<DateTimePickerLabels>The strings: time "Time", timeFor(day) "Time for {day}", noSlots "No free times on this day", pickDay, loading, retry "Try again", timeZone "Time zone", done "Done", clear "Clear", dialog "Choose date and time", presets "Quick picks", inOneHour "In 1 hour", tomorrowMorning "Tomorrow 9:00", nextMonday "Monday 9:00", selected(text), noTime "--:--". DEFAULT_DATE_TIME_PICKER_LABELS holds the defaults.
fieldPropsPartial<FieldProps>Anything else for the Field wrapper: optional, labelAction, orientation, hideLabel.
idstringThe trigger's id; generated when left out.
classNamesPartial<Record<"field" | "label" | "description" | "error" | "counter" | "box" | "trigger" | "icon" | "value" | "clear" | "chevron" | "content" | "presets" | "preset" | "body" | "calendar" | "times" | "slot" | "footer" | "zone" | "done", string>>Extra classes for the inner parts.
classNamestringExtra classes for the outermost element: the Field when there is a label or message, else the box.
refRef<HTMLButtonElement> (a useRef object or an (el) => void callback)The trigger button. Other button props (aria-*, data-*, tabIndex) land on it too.
Parts
DateTimePickerPanel
componentThe panel alone (presets, calendar, times, footer) with the same options plus value / defaultValue / onValueChange, day / onDayChange (the day on show before a time is picked, "YYYY-MM-DD"), layout ("popover" | "sheet" | "inline"), onDone and onPresetPick. Props: DateTimePickerPanelProps.
DEFAULT_DATE_TIME_PICKER_LABELS
constantThe English strings, to spread under a translation. Type: DateTimePickerLabels.
zonedTimeToDate
functionA wall time in a zone to the absolute moment, DST safe: zonedTimeToDate("2026-09-24", "16:40", "America/New_York"). Repeated hours give the first occurrence. Signature: (dateISO: string, time: string, timeZone?: string) => Date.
getZonedParts
functionThe wall clock of a moment in a zone: { date: "2026-09-24", time: "16:40" }. Signature: (date: Date, timeZone?: string) => { date: string; time: string }.
formatTimeZoneLabel
functionThe zone's name and its offset at that date: "Romania Time (UTC+03:00)". Signature: (timeZone: string, date: Date, locale?: string) => string.
buildTimeSlots
functionThe "HH:mm" list every step minutes from start to end. Signature: ({ step?, start?, end? }) => string[].
isValidTimeZone
functionWhether the runtime knows an IANA zone ("" counts as the device zone). Signature: (timeZone: string) => boolean.
Examples
Send later
Presets (in 1 hour, tomorrow 9:00, Monday 9:00), 30 minute steps, clearable, brand tone.
Time zones
Pick in New York, switch the zone from the footer select, read the same moment in London and Bucharest.
Clinic appointments
getTimeSlots returns a promise per day (600 ms): skeleton slots, then free chairs as labels.
Inline panel
No trigger: the calendar and an 8:00 AM to 8:00 PM time column inside the page.
Phone sheet
The sheet layout: presets and times as chip rails under a full width calendar, Done commits.
DST day
March 8, 2026 in New York: the wall times the clock skips are disabled.
Locales
The same value in en-US (12h, New York time) and ro-RO (24h, Bucharest time).
Dependencies
Files
- components/ui/date-time-picker.tsx
FAQ
Is the React Date Time Picker component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Date Time Picker?
Run npx wingo-ui@latest add date-time-picker. It also adds the Wingo UI items it builds on (Button, Calendar, Drawer, Field, Select and 8 more) and the npm packages @radix-ui/react-popover, date-fns, motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/date-time-picker.
How does Date Time Picker behave on phones?
Below 768px the panel opens as a bottom sheet with chip rails and a draft committed by Done. The responsive prop controls this and is on by default.
See also
Used in
More React input components
Articles using this component
- Shadcn Date Time Picker with Time Zones and Presets
Component guides9 min read
- React Time Picker: Typed Input, 12 or 24 Hours by Locale
Component guides8 min read