# Time Picker

> A React time picker to type a time or pick it from columns, with 12 / 24 hours by locale, minute steps, limits and wheels 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.0
- Install: `npx wingo-ui@latest add time-picker`
- Docs: https://wingo-ui.com/components/time-picker

## Interaction

Type a time the way you would say it: "1430", "14.30", "9", "2:30pm" or "2p" (a colon appears after the first two digits). It is read on blur and on Enter; text that does not parse turns the field red while you type and goes back to the last good time on blur, a time outside min / max moves to the nearest limit, and with strictStep the minutes snap to the step. ArrowUp / ArrowDown step the minutes by minuteStep and Shift+ArrowUp / ArrowDown step an hour, committing at once and skipping blocked times. Clicking the field or the clock button opens a panel of columns (hours, minutes, seconds, AM / PM) with the chosen time centered; clicking an option sets it at once, and a minute (or second) click closes the panel. Alt+ArrowDown (or Enter on the clock button) opens it with focus on the hour: ArrowUp / ArrowDown move and pick, PageUp / PageDown jump five, Home / End go to the ends, ArrowLeft / ArrowRight switch columns, Enter confirms and closes, Escape closes and returns to the field. On a phone the field never raises the keyboard: a tap opens a bottom sheet with wheels that snap like iOS, tick under the thumb where the phone supports vibration, and commit with Done; dragging the sheet away keeps the old time. The wheels also take arrows, PageUp / PageDown, Home and End.

## Installation

```bash
npx wingo-ui@latest add 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/time-picker` (needs the @wingo-ui registry with your API key in components.json)

Files:

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

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

Wingo UI dependencies (installed automatically):

- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.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))
- [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))
- [Date utils](https://wingo-ui.com/components/date-utils) ([Markdown](https://wingo-ui.com/components/date-utils.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 { TimePicker } from "@/components/ui/time-picker"

const lunchBreak = (time: string) => time >= "13:00" && time < "14:00"

export function PickupTime() {
  const [time, setTime] = useState("11:30")
  return (
    <TimePicker
      label="Pickup time"
      description="The store is open from 9 AM to 6 PM."
      value={time}
      onValueChange={setTime}
      locale="en-US"
      min="09:00"
      max="18:00"
      minuteStep={15}
      disabledTimes={lunchBreak}
      clearable
      showNow
      name="pickup_time"
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label?` | `ReactNode` | `""` | The field label, linked to the input. Plain text also becomes the title of the phone sheet. |
| `description?` | `ReactNode` | `""` | Helper text under the field, linked with aria-describedby. |
| `error?` | `ReactNode` | `""` | Replaces the description with an animated error and marks the field invalid. |
| `required?` | `boolean` | `false` | The red mark after the label and required on the input. |
| `invalid?` | `boolean` | `false` | The invalid look and aria-invalid without a message. |
| `disabled?` | `boolean` | `false` | Dims the field, blocks typing and never opens the panel; the hidden input is not submitted. |
| `readOnly?` | `boolean` | `false` | The time stays visible and selectable but can not change: no typing, no panel, no clear button. |
| `defaultValue?` | `string` | `""` | The starting time while uncontrolled, as "HH:mm" in 24 hours ("HH:mm:ss" with seconds), like <input type="time">. A form reset returns to it. |
| `value?` | `string` |  | The time, controlled: "HH:mm" in 24 hours whatever the display ("" is empty). A value off the step shows as given and is highlighted in no column. |
| `onValueChange?` | `(value: string) => void` |  | Called with the new "HH:mm" (or "") after a typed time is read, an arrow step, a column pick, Now, Clear or the sheet's Done. |
| `placeholder?` | `string` | `""` | Empty uses "--:--" for 24 hours and "--:-- --" for 12 hours (with seconds, one more group). |
| `locale?` | `string` | `""` | How the time is written and which hour cycle auto picks: en-US 2:30 PM, en-GB 14:30. Empty uses the nearest LocaleProvider, else the page's language. |
| `hourCycle?` | `"auto" \| "h12" \| "h23"` | `"auto"` | 12 or 24 hours. auto follows the locale. The value is always 24 hours underneath, so switching keeps the time; midnight shows as 12:00 AM. |
| `minuteStep?` | `number` | `"5"` | The minutes offered in the columns and wheels and the ArrowUp / ArrowDown step. Typed minutes can be anything unless strictStep. |
| `secondStep?` | `number` | `"5"` | The seconds offered with withSeconds. |
| `strictStep?` | `boolean` | `false` | Typed minutes (and seconds) snap to the nearest step on blur: 14:37 becomes 14:35 with a 5 minute step. |
| `withSeconds?` | `boolean` | `false` | Values become "HH:mm:ss" and a seconds column (and wheel) appears. |
| `min?` | `string` | `""` | The earliest time ("09:00"). Earlier options are disabled and typed times are moved up to it. A min later than max is an overnight window (22:00 to 06:00) that wraps past midnight. |
| `max?` | `string` | `""` | The latest time ("18:00"). Later options are disabled and typed times are moved down to it. |
| `disabledTimes?` | `(time: string) => boolean` |  | Blocks single times, called with "HH:mm" (or "HH:mm:ss"): a lunch break, booked slots. Blocked options are disabled, hours with nothing left grey out, arrow steps skip them and a typed one moves to the nearest free slot. |
| `clearable?` | `boolean` | `false` | A clear button in the field while it has a time (and a Clear button in the phone sheet). |
| `showNow?` | `boolean` | `false` | A Now button under the columns (and in the sheet) that sets the current time, rounded to the step and moved to the nearest allowed one. |
| `closeOnSelect?` | `boolean` | `true` | Desktop: a click in the minutes column (the seconds column with withSeconds) closes the panel. Hours and AM / PM never close it. The phone sheet always waits for Done. |
| `responsive?` | `boolean` | `true` | Below 768px the field opens a bottom sheet with wheels instead of the panel. Off, phones get the panel too. |
| `defaultOpen?` | `boolean` | `false` | Starts with the panel (or the sheet) open. Pass open and onOpenChange to control it. |
| `open?` | `boolean` |  | Whether the panel or sheet is open, controlled. It never opens while disabled or readOnly. |
| `onOpenChange?` | `(open: boolean) => void` |  | Called when the panel or sheet opens or closes. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | Field heights of 32, 40 and 48px (md is 44px on touch screens), with matching text. |
| `variant?` | `"soft" \| "outline" \| "underline" \| "ghost"` | `"soft"` | The field box: a tinted fill, a border, a bottom line that grows on focus, or nothing until hover. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Corner radius of the field, scaled per size like the button. Ignored by underline. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The fill of the selected options and the sheet's Done button. |
| `color?` | `string` | `""` | Any CSS color for the selection; replaces the tone. Text on it turns black or white by lightness. |
| `showColumnLabels?` | `boolean` | `false` | Small titles above the desktop columns (Hours, Minutes...) from labels. |
| `icon?` | `ReactNode` |  | The trigger button's icon; a clock by default. |
| `sheetTitle?` | `string` | `"Select time"` | The phone sheet's title when the label is not plain text (or missing). |
| `labels?` | `Partial<{ now; done; clear; open; dialog; hours; minutes; seconds; period; format: string }>` |  | Every visible and spoken string, merged over the English defaults ("Now", "Done", "Clear", "Choose time", "Hours"...). format is the screen reader hint after the field, with {format} replaced by the pattern ("Format HH:mm"). |
| `name?` | `string` | `""` | Renders a hidden input with this name and the "HH:mm" value, for native forms. The visible input has no name. |
| `side?` | `"top" \| "right" \| "bottom" \| "left"` | `"bottom"` | Where the panel opens; it flips when there is no room. |
| `align?` | `"start" \| "center" \| "end"` | `"start"` | How the panel lines up with the field. |
| `container?` | `HTMLElement \| null` |  | Portal target of the panel and the sheet; defaults to document.body. |
| `fieldProps?` | `Partial<FieldProps>` |  | Anything else for the Field wrapper: optional, labelAction, orientation, labelWidth, hideLabel. |
| `id?` | `string` |  | The input's id; generated when left out. |
| `classNames?` | `Partial<Record<"field" \| "label" \| "description" \| "error" \| "counter" \| "box" \| "input" \| "trigger" \| "clear" \| "content" \| "column" \| "option" \| "wheel" \| "band" \| "footer", string>>` |  | Extra classes for the inner parts. content is the panel (or the sheet), band the shared highlight behind the wheels. |
| `className?` | `string` |  | Extra classes for the outermost element: the Field when there is a label or message, else the box. |
| `ref?` | `Ref<HTMLInputElement> (a useRef object or an (el) => void callback)` |  | The text input. Other props (onBlur, autoFocus, aria-*, data-*) land on it too. |

## Parts

### TimePickerWheel (component)

The snapping wheel from the phone sheet, on its own: rows turn away from the center like iOS. Use it for durations, alarms or any short list of values.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `options` | `TimePickerWheelOption[]` |  | The rows: { value, label, disabled? }. |
| `value?` | `string` |  | The selected value, controlled. |
| `defaultValue?` | `string` |  | The starting value while uncontrolled. |
| `onValueChange?` | `(value: string) => void` |  | Fires once the wheel comes to rest on a new row. |
| `label` | `string` |  | The listbox's accessible name, like "Hours". |
| `band?` | `boolean` | `true` | The rounded band behind the centered row; turn it off when several wheels share one band. |
| `haptics?` | `boolean` | `true` | A short vibration tick as each row crosses the center, where the phone supports it. |
| `disabled?` | `boolean` | `false` | Blocks scrolling and keys. |
| `classNames?` | `Partial<Record<"list" \| "item" \| "band", string>>` |  | Extra classes for the list, each row and the band. |

### TimePickerWheelOption (type)

One wheel row: { value, label, disabled? }.

### TimePickerProps (type)

The props of TimePicker, for wrappers that pass them through.

## Examples

- **12 and 24 hours**: The same value in en-US (2:30 PM, with an AM / PM column) and en-GB (14:30). The value is always 24 hours.
- **Minute steps**: 1, 5, 15 and 30 minute columns; the last two snap typed minutes with strictStep.
- **With seconds**: withSeconds adds a seconds column and wheel, here every 5 seconds, with column titles and the brand tone.
- **Opening hours and a lunch break**: min / max grey out the rest of the day, disabledTimes blocks 13:00 to 14:00, and Now picks the nearest free slot.
- **Typing**: Type 1430, 14.30, 9 or 2:30pm: a colon appears after two digits and the time is read on blur or Enter.
- **Phone wheels**: TimePickerWheel on its own: scroll, drag or use the arrows. Open any picker at 390px to get the full sheet.
- **Sizes and variants**: sm, md and lg in the four field boxes.
- **Overnight window**: min after max wraps past midnight: 22:00 to 06:00, with a custom color, full radius and the outline box.

## See also

- [Calendar](https://wingo-ui.com/components/calendar) ([Markdown](https://wingo-ui.com/components/calendar.md))
- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))
- [Date utils](https://wingo-ui.com/components/date-utils) ([Markdown](https://wingo-ui.com/components/date-utils.md))
- [Input](https://wingo-ui.com/components/input) ([Markdown](https://wingo-ui.com/components/input.md))

---

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