Wheel Picker
Spin a drum of values to pick one: a time, a date or any short list, one column per part.
import { WheelPicker, WheelPickerColumn, WheelPickerOption } from '@youj/react'
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use a wheel picker where spinning is the natural gesture: touch-first forms, a timer, an alarm, a short list to choose from in place. For a time typed at a keyboard, use a Time Picker; for a long list to search, a Select or a Combobox.
import { WheelPicker } from '@youj/react'
<WheelPicker
aria-label="Alarm"
columns={[
{ id: 'hour', label: 'Hours', options: hours },
{ id: 'minute', label: 'Minutes', options: minutes },
]}
value={alarm}
onValueChange={setAlarm}
/>Each column is { id, label, options }, and each option { value, label, disabled? }. With columns, the value is an object of each column's value by its id. One column has a shorthand: options and label on the picker, with a plain string value.
<WheelPicker label="Fruit" options={fruit} defaultValue="cherry" />It is uncontrolled with defaultValue (a column left out starts on its first row it can rest on), or controlled with value and onValueChange. The value is committed when a column is let go, so onValueChange fires once per spin, not on every row it passes.
size (sm, default or lg) sets the row height and type, following the theme's control heights. The picker keeps its size while it turns: each column is as wide as its widest label.
Description
An alarm in three columns: hours, minutes in fives, and AM or PM.
Drag a column, or flick it and let it coast onto a row. The mouse wheel and trackpad turn it too, and a click on a row brings it to the band. Each column wraps neither way here; pass loop to make every column run round.
A date of birth: month, day and year.
The day column always has 31 rows, so it never changes length as the month turns; the days a month lacks are disabled, and the wheel won't rest on them. The app clamps the day when the month changes.
Code
wheel-picker-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propscolumns or options, value, loop, size, disabled, sound…
Props
| Prop | Type | |
|---|---|---|
columns | WheelPickerColumn[]The wheels, start to end. | – |
value | Record<string, string>The chosen value of each column, by column id. Controlled. | – |
defaultValue | Record<string, string>The starting values when uncontrolled. A column left out starts on its first row it can rest on. | – |
onValueChange | (value: Record<string, string>) => void | – |
options | neverOne wheel's rows: the shorthand for a single column. | – |
label | neverThe wheel's accessible name. Default "Value" (translated). | – |
loop | booleanEvery column wraps around: past the last row comes the first. | – |
size | WheelPickerSize'default''sm''lg'The row height and type size. default `default` | `default` |
disabled | booleanBlocks dragging, scrolling and keys, and takes the columns out of the tab order. | – |
sound | booleanWith sound on ( default true | true |
Data attributes
The root carries data-size, plus data-yj-loop with loop and data-disabled when disabled. Each column is wheel-picker-column with data-id (its column id) and data-dragging while a pointer holds it; each drawn row is wheel-picker-item with data-index, and data-selected in the band and data-disabled when it can't be chosen. The band is wheel-picker-band.
MotionDrag, flick, wheel · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| Dragthe column follows the pointer 1:1; past either end it pulls less the further it goes | no token | ||
| Flickthe column coasts on with the flick’s momentum and decays onto the row it reaches | duration.slow340ms | 340ms | |
| Let go past an endthe column springs back to the first or last row | spring.smooth0.36s, bounce 0 | 0.36s, bounce 0 | |
| Key, click, wheel or new valuethe column turns to the row | spring.smooth0.36s, bounce 0 | 0.36s, bounce 0 | |
| Rowsrows curve away on a drum and fade towards the top and bottom; the row in the band is full strength | no token |
Reduced motion
A drag still follows the pointer. There is no momentum: a release, key, click or new value puts the row in the band at once.
Reduced motion
Dragging works the same. A release lands on the nearest row with no momentum, and keys, clicks and new values turn the column at once.
Sound
With sound on (YoujProvider's sound), every row that passes the band ticks. sound={false} keeps the picker silent.
AccessibilitySpinbuttons, keyboard, type-ahead
Accessibility
- Each column is a
spinbuttonnamed by itslabel, its value text the chosen row's label, so a screen reader reads "Hours, 7" and each new row as it turns. Give the picker anaria-labeltoo when it has more than one column; it is agroup. - ArrowUp turns to the next row and ArrowDown to the one before; Page Up and Page Down turn five rows; Home and End go to the first and last. Disabled rows are skipped.
- Typing jumps to the next row whose label starts with what was typed; the same letter again moves on to the next match.
- ArrowLeft and ArrowRight move between columns, mirrored in right-to-left, where the columns run from the right too.
- Each column is one tab stop with a visible focus ring.
disabledtakes the columns out of the tab order and setsaria-disabled. - Touch, pen and mouse drag; each pointer is captured, so a drag keeps going outside the column. The wheel turns the column under the pointer without scrolling the page.