# Date Time Picker

> 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.

- Type: React component (Inputs)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.1
- Install: `npx wingo-ui@latest add date-time-picker`
- Docs: https://wingo-ui.com/components/date-time-picker

## Interaction

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.

## Installation

```bash
npx wingo-ui@latest add date-time-picker
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/date-time-picker` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/date-time-picker.tsx`

npm dependencies: `npm i @radix-ui/react-popover date-fns motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Calendar](https://wingo-ui.com/components/calendar) ([Markdown](https://wingo-ui.com/components/calendar.md))
- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))
- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [Select](https://wingo-ui.com/components/select) ([Markdown](https://wingo-ui.com/components/select.md))
- [Date utils](https://wingo-ui.com/components/date-utils) ([Markdown](https://wingo-ui.com/components/date-utils.md))
- [Relative time](https://wingo-ui.com/components/relative-time) ([Markdown](https://wingo-ui.com/components/relative-time.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
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

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label?` | `ReactNode` | `""` | The field label. Plain text also titles the phone sheet. |
| `description?` | `ReactNode` | `""` | Helper text under the field, linked with aria-describedby. |
| `error?` | `ReactNode` | `""` | Replaces the description with an error and marks the field invalid. |
| `required?` | `boolean` | `false` | The required mark after the label and aria-required on the trigger. |
| `invalid?` | `boolean` | `false` | The invalid look and aria-invalid without a message. |
| `disabled?` | `boolean` | `false` | Dims the field and never opens the panel; the hidden input is not submitted. |
| `readOnly?` | `boolean` | `false` | Shows the value but never opens the panel (inline: picks are ignored). |
| `value?` | `Date \| null` |  | The 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. |
| `defaultValue?` | `Date \| null` | `"null"` | The starting moment while uncontrolled. A form reset returns to it. |
| `onValueChange?` | `(value: Date \| null) => void` |  | Called when a slot or a preset is picked, when a new day keeps the chosen time, on Clear, and on the sheet's Done. |
| `minuteStep?` | `number` | `15` | Minutes between slots when getTimeSlots is not set; also rounds the In 1 hour preset. |
| `startTime?` | `string` | `"00:00"` | The first slot of the day, "HH:mm" ("08:00" for business hours). |
| `endTime?` | `string` | `"23:59"` | The 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 list` |  | Availability 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) => boolean` |  | Disables single slots by their absolute moment. |
| `min?` | `Date` |  | Days and slots before are disabled (the day of min stays open for its later slots). |
| `max?` | `Date` |  | Days and slots after are disabled. Ignored when it is before min. |
| `disablePast?` | `boolean` | `false` | Shorthand for min = now: past days and today's past slots are disabled. |
| `disabledDays?` | `DateMatcher 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. |
| `presets?` | `boolean \| DateTimePreset[]` | `false` | 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. |
| `timeZone?` | `string` | `""` | The 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. |
| `showTimeZone?` | `boolean` | `true` | The zone label in the footer. |
| `timeZones?` | `string[]` |  | Makes the zone label a select to switch zones; the moment stays, the wall clock moves. |
| `onTimeZoneChange?` | `(zone: string) => void` |  | Called when a zone is picked from the select. |
| `hourCycle?` | `"auto" \| "h12" \| "h23"` | `"auto"` | 12 or 24 hour slots and trigger text; auto follows the locale. |
| `displayFormat?` | `Intl.DateTimeFormatOptions` | `"{ weekday: \"short\", day: \"numeric\", month: \"short\", year: \"numeric\", hour: \"2-digit\", minute: \"2-digit\" }"` | How the trigger writes the value ("Thu, Sep 24, 2026, 4:40 PM"). While only a day is picked its date part is shown with --:--. |
| `placeholder?` | `string` | `"Pick date and time"` | The trigger text while empty. |
| `clearable?` | `boolean` | `false` | A clear button in the field while it has a value (and Clear in the sheet). |
| `inline?` | `boolean` | `false` | Renders the panel in the page without a trigger. |
| `weekStartsOn?` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| -1` | `-1` | The calendar's first weekday (0 Sunday to 6 Saturday); -1 uses the locale's. |
| `calendarProps?` | `Partial<CalendarProps>` |  | Anything else for the calendar: size, radius, events, showWeekNumbers, labels. |
| `now?` | `Date \| number` |  | What now means for presets and disablePast. Default: the clock, ticking each minute (null until hydrated). |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The selected day, slot and preset, and the Done button. |
| `color?` | `string` | `""` | Any CSS color; replaces the tone. |
| `icon?` | `ReactNode` |  | The trigger's leading icon. Default: CalendarClock. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | The field box: 32 / 40 / 48px (md grows to 44px on touch). |
| `variant?` | `"soft" \| "outline" \| "underline" \| "ghost"` | `"soft"` | The field box look. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | The field box corners. |
| `name?` | `string` | `""` | Submits value.toISOString() through a hidden input. |
| `defaultOpen?` | `boolean` | `false` | Starts with the panel (or the sheet) open. |
| `open?` | `boolean` |  | Whether the panel is open, controlled. It never opens while disabled or readOnly. |
| `onOpenChange?` | `(open: boolean) => void` |  | Called when the panel opens or closes. |
| `responsive?` | `boolean` | `true` | Below 768px the panel opens as a bottom sheet with chip rails and a draft committed by Done. |
| `sheetTitle?` | `string` | `""` | The sheet title; empty uses the label's text, else labels.dialog. |
| `side?` | `"top" \| "right" \| "bottom" \| "left"` | `"bottom"` | Where the popover opens (it flips when there is no room). |
| `align?` | `"start" \| "center" \| "end"` | `"start"` | The popover's alignment to the field. |
| `container?` | `HTMLElement \| null` |  | Where the popover or sheet portals to. |
| `locale?` | `string` | `""` | Month 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. |
| `labels?` | `Partial<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. |
| `fieldProps?` | `Partial<FieldProps>` |  | Anything else for the Field wrapper: optional, labelAction, orientation, hideLabel. |
| `id?` | `string` |  | The trigger's id; generated when left out. |
| `classNames?` | `Partial<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. |
| `className?` | `string` |  | Extra classes for the outermost element: the Field when there is a label or message, else the box. |
| `ref?` | `Ref<HTMLButtonElement> (a useRef object or an (el) => void callback)` |  | The trigger button. Other button props (aria-*, data-*, tabIndex) land on it too. |

## Parts

### DateTimePickerPanel (component)

The 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 (constant)

The English strings, to spread under a translation. Type: DateTimePickerLabels.

### zonedTimeToDate (function)

A 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 (function)

The 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 (function)

The zone's name and its offset at that date: "Romania Time (UTC+03:00)". Signature: (timeZone: string, date: Date, locale?: string) => string.

### buildTimeSlots (function)

The "HH:mm" list every step minutes from start to end. Signature: ({ step?, start?, end? }) => string[].

### isValidTimeZone (function)

Whether 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).

## See also

- [Date Picker](https://wingo-ui.com/components/date-picker) ([Markdown](https://wingo-ui.com/components/date-picker.md))
- [Time Picker](https://wingo-ui.com/components/time-picker) ([Markdown](https://wingo-ui.com/components/time-picker.md))
- [Calendar](https://wingo-ui.com/components/calendar) ([Markdown](https://wingo-ui.com/components/calendar.md))
- [Date Range Picker](https://wingo-ui.com/components/date-range-picker) ([Markdown](https://wingo-ui.com/components/date-range-picker.md))
- [Timezone Select](https://wingo-ui.com/components/timezone-select) ([Markdown](https://wingo-ui.com/components/timezone-select.md))
- [Scheduler](https://wingo-ui.com/components/scheduler) ([Markdown](https://wingo-ui.com/components/scheduler.md))

---

Canonical: https://wingo-ui.com/components/date-time-picker
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
