Time Picker React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
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 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.
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 time-picker, together with Button, Drawer, Field, useControllableState, useLocale and 5 more and the npm packages @radix-ui/react-popover, 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 time-pickerTime 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 TimePicker from @/components/ui/time-picker.
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
labeldefault""ReactNodeThe field label, linked to the input. Plain text also becomes the title of the phone sheet.
descriptiondefault""ReactNodeHelper text under the field, linked with aria-describedby.
errordefault""ReactNodeReplaces the description with an animated error and marks the field invalid.
requireddefaultfalsebooleanThe red mark after the label and required on the input.
invaliddefaultfalsebooleanThe invalid look and aria-invalid without a message.
disableddefaultfalsebooleanDims the field, blocks typing and never opens the panel; the hidden input is not submitted.
readOnlydefaultfalsebooleanThe time stays visible and selectable but can not change: no typing, no panel, no clear button.
defaultValuedefault""stringThe 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.
valuestringThe 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) => voidCalled 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.
placeholderdefault""stringEmpty uses "--:--" for 24 hours and "--:-- --" for 12 hours (with seconds, one more group).
localedefault""stringHow 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.
hourCycledefault"auto""auto" | "h12" | "h23"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.
minuteStepdefault"5"numberThe minutes offered in the columns and wheels and the ArrowUp / ArrowDown step. Typed minutes can be anything unless strictStep.
secondStepdefault"5"numberThe seconds offered with withSeconds.
strictStepdefaultfalsebooleanTyped minutes (and seconds) snap to the nearest step on blur: 14:37 becomes 14:35 with a 5 minute step.
withSecondsdefaultfalsebooleanValues become "HH:mm:ss" and a seconds column (and wheel) appears.
mindefault""stringThe 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.
maxdefault""stringThe latest time ("18:00"). Later options are disabled and typed times are moved down to it.
disabledTimes(time: string) => booleanBlocks 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.
clearabledefaultfalsebooleanA clear button in the field while it has a time (and a Clear button in the phone sheet).
showNowdefaultfalsebooleanA 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.
closeOnSelectdefaulttruebooleanDesktop: 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.
responsivedefaulttruebooleanBelow 768px the field opens a bottom sheet with wheels instead of the panel. Off, phones get the panel too.
defaultOpendefaultfalsebooleanStarts with the panel (or the sheet) open. Pass open and onOpenChange to control it.
openbooleanWhether the panel or sheet is open, controlled. It never opens while disabled or readOnly.
onOpenChange(open: boolean) => voidCalled when the panel or sheet opens or closes.
sizedefault"md""sm" | "md" | "lg"Field heights of 32, 40 and 48px (md is 44px on touch screens), with matching text.
variantdefault"soft""soft" | "outline" | "underline" | "ghost"The field box: a tinted fill, a border, a bottom line that grows on focus, or nothing until hover.
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"Corner radius of the field, scaled per size like the button. Ignored by underline.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The fill of the selected options and the sheet's Done button.
colordefault""stringAny CSS color for the selection; replaces the tone. Text on it turns black or white by lightness.
showColumnLabelsdefaultfalsebooleanSmall titles above the desktop columns (Hours, Minutes...) from labels.
iconReactNodeThe trigger button's icon; a clock by default.
sheetTitledefault"Select time"stringThe phone sheet's title when the label is not plain text (or missing).
labelsPartial<{ 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").
namedefault""stringRenders a hidden input with this name and the "HH:mm" value, for native forms. The visible input has no name.
sidedefault"bottom""top" | "right" | "bottom" | "left"Where the panel opens; it flips when there is no room.
aligndefault"start""start" | "center" | "end"How the panel lines up with the field.
containerHTMLElement | nullPortal target of the panel and the sheet; defaults to document.body.
fieldPropsPartial<FieldProps>Anything else for the Field wrapper: optional, labelAction, orientation, labelWidth, hideLabel.
idstringThe input's id; generated when left out.
classNamesPartial<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.
classNamestringExtra classes for the outermost element: the Field when there is a label or message, else the box.
refRef<HTMLInputElement> (a useRef object or an (el) => void callback)The text input. Other props (onBlur, autoFocus, aria-*, data-*) land on it too.
Parts
TimePickerWheel
componentThe 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.
options*TimePickerWheelOption[]The rows: { value, label, disabled? }.
valuestringThe selected value, controlled.
defaultValuestringThe starting value while uncontrolled.
onValueChange(value: string) => voidFires once the wheel comes to rest on a new row.
label*stringThe listbox's accessible name, like "Hours".
banddefaulttruebooleanThe rounded band behind the centered row; turn it off when several wheels share one band.
hapticsdefaulttruebooleanA short vibration tick as each row crosses the center, where the phone supports it.
disableddefaultfalsebooleanBlocks scrolling and keys.
classNamesPartial<Record<"list" | "item" | "band", string>>Extra classes for the list, each row and the band.
TimePickerWheelOption
typeOne wheel row: { value, label, disabled? }.
TimePickerProps
typeThe 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.
Dependencies
Files
- components/ui/time-picker.tsx
FAQ
Is the React Time Picker component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Time Picker?
Run npx wingo-ui@latest add time-picker. It also adds the Wingo UI items it builds on (Button, Drawer, Field, useControllableState, useLocale and 5 more) and the npm packages @radix-ui/react-popover, motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/time-picker.
How does Time Picker behave on phones?
Below 768px the field opens a bottom sheet with wheels instead of the panel. Off, phones get the panel too. The responsive prop controls this and is on by default.
See also
Used in
More React input components
Articles using this component
- React Appointment Booking That Gets Time Zones Right
Build it: screens and apps9 min read
- 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
- React Wheel Picker: iOS Wheels for Times and Dates
Mobile UI in React8 min read