Picker Wheel React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React iOS-style picker wheel whose columns spin with momentum and snap on a 3D cylinder.
Flick a column with a finger and it spins with the phone's own momentum, rows tilting away on a cylinder and fading toward the edges, then clicks into place on a row; each row that reaches the center ticks under the thumb where the phone can vibrate (Android, iOS 17.4+). The value changes once, when the wheel comes to rest, never per row passed; a wheel that lands on a disabled (sold out) row glides on to the nearest one that is not. With a mouse, scroll the wheel, click-drag it up or down and let go to keep it spinning with inertia, or click any visible row to bring it to the center. Each column is one Tab stop: ArrowUp / ArrowDown move a row, PageUp / PageDown five, Home / End jump to the ends, typing jumps to the next option that starts with the text ("19" then "199" on a year wheel), ArrowLeft / ArrowRight move to the neighbor column. Looping columns wrap around from the last row to the first. PickerWheelField shows the value in a form field: a tap opens a bottom sheet on phones with the wheels, Done on top and Cancel under it (Enter on a wheel works like Done; dragging the sheet away keeps the old value), a click or Enter / Space / Alt+ArrowDown opens a popover on desktop where the wheel applies live and Enter or Done closes it, Escape closes and returns focus to the field; the field is announced with its label and its current value. With reduced motion the rows stay flat and programmatic moves jump instead of gliding.
Picker Wheel is a Pro React component in the React mobile components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add picker-wheel, together with Button, Drawer, Field, useControllableState, useLocale and 6 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 picker-wheelPicker Wheel 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 PickerWheel from @/components/ui/picker-wheel.
import { useState } from "react"
import { PickerWheel, PickerWheelField, pickerRange, useDateWheel } from "@/components/ui/picker-wheel"
const hours = pickerRange(8, 19, { pad: 2 })
const minutes = pickerRange(0, 55, { step: 5, pad: 2 })
export function Appointment() {
const [time, setTime] = useState(["10", "30"])
const birthday = useDateWheel({ defaultValue: "1990-05-17", locale: "en-US" })
return (
<div className="flex flex-col gap-6">
<PickerWheel aria-label="Date of birth" {...birthday} />
<PickerWheel
aria-label="Appointment time"
loop
value={time}
onValueChange={(value) => setTime(value)}
columns={[
{ id: "hour", label: "h", options: hours },
{ id: "minute", label: "min", options: minutes },
]}
/>
<PickerWheelField
label="Model year"
name="year"
placeholder="Choose the year"
options={pickerRange(2026, 1990)}
/>
</div>
)
}Props
columnsPickerWheelColumnDef[]The wheels, left to right: { id, options, label?, width?: "auto" | number, align?: "start" | "center" | "end", loop?, showLabel? }. label names the listbox and shows beside the center row ("h", "min"). Required unless options is set.
optionsPickerWheelOption[]Shortcut for a single column. Each option is { value, label, textValue?, disabled? }; textValue feeds type-to-jump and the field text when the label is not plain text.
valuestring[]One value per column, in column order (controlled). A value that is not an option shows the first enabled option and fires nothing until the user moves.
defaultValuestring[]The starting values while uncontrolled; defaults to the first enabled option of each column.
onValueChange(value: string[], column: string) => voidFires once per settle with every column's value and the id of the column that moved, never per row passed.
sizedefault"md""sm" | "md" | "lg"Row height 36 / 44 / 52px with 15 / 17 / 20px text. md matches the wheels of the time picker's sheet.
visibleRowsdefault5numberHow many rows show (odd, 3 to 9). The wheel is rowHeight x visibleRows tall: 220px at md with 5 rows.
perspectivedefault"ios""ios" | "subtle" | "flat"How far rows tilt away from the center: 20 / 10 / 0 degrees per row, capped at 55. Opacity falls to 0.35 either way.
banddefaulttruebooleanThe rounded selection band behind the center row, shared by all columns.
fadedefaulttruebooleanFades the top and bottom rows out with a mask, like the iOS picker.
showColumnLabelsdefaulttruebooleanShows each column's label right after its widest option, beside the center row ("2 h 30 min"). A column's own showLabel wins; date columns from useDateWheel turn it off.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"neutral: a light gray band and foreground text. Other tones tint the band and color the centered row.
colordefault""stringAny CSS color; replaces the tone for the band and the centered row.
disableddefaultfalsebooleanDims the wheel, stops scrolling and removes the columns from the Tab order.
loopdefaultfalsebooleanEvery column wraps around, so 59 is followed by 00 again. A column's own loop wins; lists shorter than half the window never loop.
hapticsdefaulttruebooleanA selection tick each time a new row reaches the center while a finger, a mouse drag or a wheel drives it; never for keys, clicks or outside value changes.
aria-labeldefault""stringThe name of the group of wheels ("Date of birth"); each column is named by its own label.
labelsPartial<PickerWheelLabels>Strings: empty "No options", and for the field done "Done", cancel "Cancel", placeholder "Select". Defaults in DEFAULT_PICKER_WHEEL_LABELS.
classNamesPartial<Record<"band" | "column" | "option" | "columnLabel", string>>Extra classes for the band, each column listbox, each option row and the column labels.
classNamestringExtra classes for the root, merged last (width, margins, tone var overrides).
Parts
PickerWheelField
componentThe wheel as a form field: everything PickerWheel takes plus label, description, error, required, invalid, name (a hidden input with the values joined by formatValue or ","), placeholder, variant ("soft" | "outline" | "ghost"), radius, responsive (a bottom sheet below 768px, default true), open / defaultOpen / onOpenChange, sheetTitle, container and formatValue(value) for the text in the box. The sheet commits on Done (or Enter on a wheel); the desktop popover applies live and closes on Enter or Done. Either way the wheel gets focus on open and the field gets it back on close. Props: PickerWheelFieldProps.
PickerWheelColumn
componentOne wheel on its own, with the time picker wheel's shape: options, value / defaultValue / onValueChange(value: string), label, showLabel, align, loop and the appearance props (size, visibleRows, perspective, band, fade, tone, color, haptics, disabled). Props: PickerWheelColumnProps.
useDateWheel
hookDay, month and year columns in the locale's order (ro-RO day / month / year, en-US month / day / year), month names in the locale, days that follow the month (29.02 only in leap years; a day that no longer exists becomes the last one) and years between min and max (ISO dates, default 1900-01-01 to 2100-12-31). Values are local ISO dates ("2026-09-24"). Spread it: <PickerWheel {...useDateWheel({ defaultValue: "1990-05-17", locale: "ro-RO" })} />; date is the current ISO date (not spread). Signature: (options: { value?, defaultValue?, onValueChange?(date: string), locale?, min?, max?, order?, labels? }) => { columns, value, onValueChange, date }.
pickerRange
functionNumbered options either way: pickerRange(0, 23, { pad: 2 }) gives "00" to "23"; pickerRange(0, 55, { step: 5, pad: 2 }) gives minutes every 5; format(n) makes the label, disabled(n) blocks a number. Signature: (start: number, end: number, options?: { step?, pad?, format?, disabled? }) => PickerWheelOption[].
DEFAULT_PICKER_WHEEL_LABELS
constantThe English defaults of labels, for translating once. Type: PickerWheelLabels.
Examples
Time, 24h with loop
Hours 08 to 19 and minutes every 5 that wrap around, labeled h / min beside the center row.
Quantity
One large column from 1 to 99 with a running total that updates when the wheel settles.
As a form field
PickerWheelField: a bottom sheet with Done / Cancel on phones, a popover that applies live on desktop.
Sizes and sold-out options
sm / md / lg rows (36 / 44 / 52px). Disabled options are struck through and the wheel glides past them.
Perspectives
ios, subtle and flat tilt, seven visible rows. Reduced motion always renders flat.
Duration in a tone
tone="brand" tints the band and the centered rows; the labels sit right after the widest number.
Locale order
useDateWheel follows the locale: day, month, year in ro-RO and month, day, year in en-US, with the month names of each.
Dependencies
Files
- components/ui/picker-wheel.tsx
FAQ
Is the React Picker Wheel component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Picker Wheel?
Run npx wingo-ui@latest add picker-wheel. It also adds the Wingo UI items it builds on (Button, Drawer, Field, useControllableState, useLocale and 6 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/picker-wheel.
How does Picker Wheel behave on phones?
PickerWheelField shows the value in a form field: a tap opens a bottom sheet on phones with the wheels, Done on top and Cancel under it (Enter on a wheel works like Done; dragging the sheet away keeps the old value), a click or Enter / Space / Alt+ArrowDown opens a popover on desktop where the wheel applies live and Enter or Done closes it.
See also
More React mobile components
Articles using this component
- Konsta UI Alternative for React: Ionic vs Wingo UI
Comparisons and alternatives10 min read
- React Time Picker: Typed Input, 12 or 24 Hours by Locale
Component guides8 min read
- Best React Component Library for Mobile in 2026: 9 Compared
Comparisons and alternatives12 min read
- React Wheel Picker: iOS Wheels for Times and Dates
Mobile UI in React8 min read