An iOS-style drum: drag or flick it and it coasts onto a row, ticking the selection band as rows pass. Built on the gesture kit and inertia core.
When to use
- Times, durations, dates, quantities and short lists picked by thumb on touch screens.
- Several linked values (hour, minute, AM/PM) behind one band with
WheelPickerGroup.
When not to use
- Long lists that need search: use a Combobox.
- Desktop forms where typing is faster: use a Select or a Number field.
Installation
npx shadcn add @eased-pro/wheel-pickerNeeds the @eased-pro registry and your Pro token in components.json. Create a token and copy the setup from your account .
Usage
import { WheelPicker, WheelPickerGroup } from "@/components/eased/wheel-picker/wheel-picker";export function Example() { const [hour, setHour] = useState(7); const [minute, setMinute] = useState(30); return ( <WheelPickerGroup aria-label="Time" className="w-56"> <WheelPicker aria-label="Hour" options={[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]} value={hour} onValueChange={setHour} align="end" className="flex-1" /> <WheelPicker aria-label="Minute" options={Array.from({ length: 12 }, (_, i) => ({ value: i * 5, label: String(i * 5).padStart(2, "0") }))} value={minute} onValueChange={setMinute} className="flex-1" /> </WheelPickerGroup> );}API reference
WheelPicker
| Prop | Type | Default | Description |
|---|---|---|---|
options | (WheelOption | string | number)[] | { value, label?, textValue?, disabled? }. Disabled rows are skipped. | |
value / defaultValue | V | first enabled | Controlled or uncontrolled. |
onValueChange | (value: V) => void | When the wheel rests on a new row, or a key selects one. | |
onTick | (value: V) => void | Each row crossing the centre: wire a sound here. | |
haptics | boolean | false | navigator.vibrate per tick where supported. |
itemHeight | number | 36 | Row height, px. |
visibleCount | number | 5 | Rows in view (odd). |
align | "start" | "center" | "end" | "center" | |
aria-label | string | Required. | |
disabled | boolean | false |
WheelPickerGroup
itemHeight, visibleCount, aria-label, className. Draws one band behind its wheels and sets their geometry.
Keyboard
| Key | Action |
|---|---|
Tab | Moves between wheels. |
↑ / ↓ | Previous or next row. |
Page Up / Page Down | Five rows. |
Home / End | First or last row. |
| Letters | Type-ahead to the next matching row. |
Accessibility
- Each wheel is a
spinbuttonwitharia-valuetextset to the row's text; the group is a namedgroup. - The rows are decorative (
aria-hidden); the value text is what is read. - Mouse wheel and trackpad scroll the drum and settle on a row.
Motion
Rows sit on a cylinder (translateZ and rotateX, one MotionValue, no re-renders), fading with the cosine of their angle. A flick coasts with UIScrollView friction (decay at 0.998) and eases onto a row; a slow release (under 100px/s) or a pause in wheel scrolling settles on the nearest row with the snappy spring; a drag past the ends rubber-bands. Each row crossing the centre bumps the band (scaleY 0.9 → 1 over 180ms). Keys and taps glide on a 300ms spring. Reduced motion: a flat list with no perspective and no band bump, and every move jumps.
Notes for AI
Choose it for touch-first pickers of short ordered lists. Compose time or date pickers from several wheels in a WheelPickerGroup; give each wheel a clear aria-label.
Do not wrap it in a scroll container or add scroll snapping; it handles drag, wheel, keys and snapping itself. It is not a listbox: do not add role="option" children.
Related
- Segmented: two to five choices, all visible.
- Scrub Label: drag to change a number without a list.