Wheel picker

Pick from a vertical list with native scroll snapping, keyboard support and optional looping.

Scroll, use the keyboard or type to jump to an option. Works in React, Vue, Svelte, Solid, Preact and vanilla JS.

  • Native scroll snapping, so touch momentum and trackpads feel right
  • Keyboard, page keys and typeahead
  • Optional looping
  • Disabled items, controlled and uncontrolled values
  • A hidden input for forms

Installation

in the preview, then copy the prompt into Claude Code, Cursor or another coding agent, or add the component yourself with the shadcn CLI or by hand.

Prompt for your coding agent
Show prompt
Add the wheel picker from isbatak-labs to this project. It's a scrollable wheel for picking one value from a list, built on native scroll snapping with optional looping. Use the React version styled with Panda CSS and built on the Zag state machine. Docs: https://isbatak-labs.vercel.app/components/prototypes/wheel-picker.md

1. Install it with the shadcn CLI. Swap `pnpm dlx` for `npx`, `yarn dlx` or `bunx` to match the project's lockfile:

pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-react-panda.json

This installs `@isbatak/zag-wheel-picker` and `@zag-js/react`, plus `@isbatak/panda-wheel-picker` as a dev dependency, and adds the component to `src/components/wheel-picker-basic/`. If the CLI can't run here, install those packages yourself and write the files listed in https://isbatak-labs.vercel.app/r/wheel-picker-basic-react-panda.json (each entry has its content and target path).

2. Finish the setup:

- Panda CSS: add `"@isbatak/panda-wheel-picker"` to the presets in the Panda config (or import `wheelPickerPreset` from it if the config already imports its presets), then run `panda codegen`. If the project doesn't use Panda CSS, stop and tell me to pick the CSS version in the docs instead.

3. Ask me where the picker should go and what it should list, and suggest a spot if one stands out. Then render it there: copy the installed component, give it a name that fits, replace the items and the label with mine, and keep the value options that fit (a default value, or a controlled value with a change handler).

4. Start the dev server, check that the picker renders, scrolls and snaps to a value, then stop it. If you can't run it, tell me how to check it myself.
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-react-panda.json

This installs the dependencies and adds the component to src/components/wheel-picker-basic/. It imports the recipe from styled-system/recipes.

Add "@isbatak/panda-wheel-picker" to presets in your Panda config, or import wheelPickerPreset from it and add that instead.

Examples

Scroll to an example to load it in the preview, and open the code view there for the full source. The settings button in the preview changes the example live, and the snippet under it follows. Each example is its own registry item, so you can add it with the CLI the same way.

Controlled value

Pass value and onValueChange to own the state, or bind the value with v-model in Vue and bind:value in Svelte. Anything that updates the value, like the buttons under the wheel, moves the picker to that item.

const picker = wheelPicker.createWheelPicker()
const api = picker.connect(
  {
    id,
    items,
    value,
    onValueChange: (details) => setValue(details.value),
  },
  normalizeProps,
)

Looping

Set loop to repeat the items, so the wheel wraps from the last item back to the first. The items render several times and the wheel jumps back to the middle copy whenever scrolling stops, so it never reaches an end.

const picker = wheelPicker.createWheelPicker()
const api = picker.connect(
  {
    id,
    items,
    value: "30",
    onValueChange: (details) => setValue(details.value),
    loop: true,
  },
  normalizeProps,
)

Styling

The viewport is a scroll container with scroll-snap-type: y mandatory, padded so the first and last rows can reach the middle. The highlight sits behind the rows and the Panda recipe fades the viewport edges with a mask. Where the browser supports scroll-driven animations, each row tilts away from the middle like a drum. The row height and the number of rows in view come from the itemHeight and visibleCount props, so size the text to fit them.

Accessibility

The viewport is a spinbutton that announces the selected label through aria-valuetext. The rows are hidden from assistive tech, since the viewport already reports the value. Arrow keys move one row, Page Up and Page Down move a page, Home and End jump to the ends, and typing jumps to the next label that matches. Disabled rows are skipped, and a flick that lands on one moves to the nearest enabled row. Give each wheel a name with aria-label or the label part.

Props

PropTypeDefault
idstring
The unique id of the wheel picker.
—
idsElementIds
The ids of the elements in the wheel picker. Useful for composition.
—
itemsWheelPickerItem[]
The items to pick from.
—
valuestring
The selected value.
—
onValueChange(details: ValueChangeDetails) => void
Function called when the selected value changes.
—
loopboolean
Whether the items repeat, so the wheel never reaches an end.
false
itemHeightnumber
The height of each row in pixels.
36
visibleCountnumber
The number of rows in view. Keep it odd so one row sits in the middle.
5
disabledboolean
Whether the wheel picker is disabled.
—
namestring
The name of the hidden input, for native forms.
—
formstring
The id of the form that owns the hidden input.
—
aria-labelstring
The accessible label for the wheel picker.
—
aria-labelledbystring
The id of the element that labels the wheel picker.
—

API

PropertyType
valuestring
The selected value.
selectedItemWheelPickerItem
The selected item.
rowsWheelPickerRow[]
The rows to render. A looping wheel renders its items several times.

Upstreaming