# 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

Pick your framework 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.

Add the component with the shadcn CLI, using the registry item for your framework, styling and API (Zag or Ark UI):

```sh
# React, Panda CSS, Zag
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-react-panda.json

# React, CSS, Zag
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-react.json

# Vue, Panda CSS, Zag
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-vue-panda.json

# Vue, CSS, Zag
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-vue.json

# Svelte, Panda CSS, Zag
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-svelte-panda.json

# Svelte, CSS, Zag
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-svelte.json

# Solid, Panda CSS, Zag
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-solid-panda.json

# Solid, CSS, Zag
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-solid.json

# Preact, Panda CSS, Zag
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-preact-panda.json

# Preact, CSS, Zag
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-preact.json

# Vanilla, Panda CSS, Zag
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-vanilla-panda.json

# Vanilla, CSS, Zag
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-vanilla.json

# React, Panda CSS, Ark UI
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-react-panda-ark.json

# React, CSS, Ark UI
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-react-ark.json

# Vue, Panda CSS, Ark UI
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-vue-panda-ark.json

# Vue, CSS, Ark UI
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-vue-ark.json

# Svelte, Panda CSS, Ark UI
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-svelte-panda-ark.json

# Svelte, CSS, Ark UI
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-svelte-ark.json

# Solid, Panda CSS, Ark UI
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-solid-panda-ark.json

# Solid, CSS, Ark UI
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-solid-ark.json

# Preact, Panda CSS, Ark UI
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-preact-panda-ark.json

# Preact, CSS, Ark UI
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/wheel-picker-basic-preact-ark.json
```

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

Preact with Ark UI uses the React components through `preact/compat`, so alias `react` and `react-dom` to `preact/compat` unless `@preact/preset-vite` already does.

## 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.

With Zag (React):

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

With Ark UI (React):

```tsx
<WheelPicker.Root
  items={items}
  value={value}
  onValueChange={(details) => setValue(details.value)}
>
  {/* … */}
</WheelPicker.Root>
```

### 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.

With Zag (React):

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

With Ark UI (React):

```tsx
<WheelPicker.Root items={items} defaultValue="30" loop>
  {/* … */}
</WheelPicker.Root>
```

## 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

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | — | The unique id of the wheel picker. |
| `ids` | `ElementIds` | — | The ids of the elements in the wheel picker. Useful for composition. |
| `items` | `WheelPickerItem[]` | — | The items to pick from. |
| `value` | `string` | — | The selected value. |
| `onValueChange` | `(details: ValueChangeDetails) => void` | — | Function called when the selected value changes. |
| `loop` | `boolean` | `false` | Whether the items repeat, so the wheel never reaches an end. |
| `itemHeight` | `number` | `36` | The height of each row in pixels. |
| `visibleCount` | `number` | `5` | The number of rows in view. Keep it odd so one row sits in the middle. |
| `disabled` | `boolean` | — | Whether the wheel picker is disabled. |
| `name` | `string` | — | The name of the hidden input, for native forms. |
| `form` | `string` | — | The id of the form that owns the hidden input. |
| `aria-label` | `string` | — | The accessible label for the wheel picker. |
| `aria-labelledby` | `string` | — | The id of the element that labels the wheel picker. |

## API

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | — | The selected value. |
| `selectedItem` | `WheelPickerItem` | — | The selected item. |
| `rows` | `WheelPickerRow[]` | — | The rows to render. A looping wheel renders its items several times. |

## Upstreaming

  The Zag.js maintainers decided the wheel picker doesn't need a machine, because native scroll snapping covers most of
  it. The machine merge request (#3434) was closed in favor of that simpler solution, and the approach became a blog
  post with a working example in the website redesign (#3432). The earlier merge requests had already been closed and
  refactored for the Zag.js v2 migration. This package follows the same approach, so there is no machine to replace.
