---
title: "JavaScript Wheel Picker"
description: "The LemonadeJS Wheel Picker block for JavaScript: iOS-style wheel picker. Contract-verified, framework-agnostic, zero dependencies, with a live example."
source: https://lemonadejs.com/docs/plugins/wheel/
---

<link rel="stylesheet" href="/v6/wheel.css">

# JavaScript Wheel Picker

`@lemonadejs/wheel` · ✓ 16 contract checks · framework-agnostic · zero dependencies

`<Wheel />` — iOS-style scroll wheel picker, ported from the v5 plugin

The v5 model, kept: a column of options behind two frosted masks; the
row in the middle band is the selection. A mouse-wheel NOTCH steps one
row; trackpad deltas glide freely and settle on the nearest row; press
and drag scrolls with the pointer (snap suspended while dragging, as
v5 toggled lm-wheel-grid) and snaps to the closest row on release.

v6 rebuild: the position is a transform driven from props (rowheight ×
visible), not native scrollTop — deterministic everywhere (jsdom has
no layout) and the snap animation is one CSS transition. v5 leaked a
document mousemove/mouseup pair per instance forever and shared one
module-global drag flag; v6 arms document listeners per gesture with
ONE persistent cleanup (the Modal track pattern), released on pointer
up and on unmount.

v5 → v6 mapping: value (the selected OPTION object, two-way) → bind
(the selected INDEX — survives attribute coercion and primitive
options; api.getValue() still returns the entry itself); onupdate →
onchange; options stays options, now also accepting plain strings and
numbers (v5 required { title } objects); the fixed 40px/200px
geometry becomes rowheight and visible. New: touch dragging, tap to
select a row, keyboard arrows/Home/End, disabled.

## Example

<!--example-->

```js
import { html } from 'lemonadejs';
import Wheel from '@lemonadejs/wheel';

const App = (props, { state }) => {
    const hour = state(9);
    const minute = state(6); // index into the options
    const hours = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0'));
    const minutes = Array.from({ length: 12 }, (_, i) => String(i * 5).padStart(2, '0'));

    return html`<div>
        <div style="display: flex; gap: 8px; align-items: center;">
            <${Wheel} bind="${hour}" options="${hours}" rowheight="32" visible="5" />
            <b>:</b>
            <${Wheel} bind="${minute}" options="${minutes}" rowheight="32" visible="5" />
        </div>
        <p>Alarm set for <b>${() => hours[hour.value] + ':' + minutes[minute.value]}</b></p>
    </div>`;
};
```

## Installation

```bash
npm install @lemonadejs/wheel
```

```js
import Wheel from '@lemonadejs/wheel';
import '@lemonadejs/wheel/style.css';
```

Three deployment forms, one component:

```js
html`<${Wheel} />`                       // by value (no registration)
setComponents({ Wheel });               // then <Wheel /> by name anywhere
createWebComponent(Wheel);              // <lm-wheel> in plain HTML/any framework
```

## Props

Every declared prop arrives as a **live state** — pass a value for a snapshot or a
state for a two-way live wire. Attribute strings are coerced to the declared type.

| Prop | Type | Default | Description |
|---|---|---|---|
| `bind` | number | — | Two-way bound value. `.set()` fires `onchange`; plain assignment is silent. two-way selected index (v5: value held the option) |
| `selected` | number | `0` | initial index when unbound |
| `options` | array | — | entries: strings/numbers or { title } objects (v5) |
| `rowheight` | number | `40` | px per row (v5: fixed 40) |
| `visible` | number | `5` | rows in the viewport (v5: fixed 200px / 40) |
| `disabled` | boolean | `false` | blocks interaction (new) |
| `aria-label` | string | `''` |  |

## Events

All event names are lowercase (the platform convention — LJS-305 warns otherwise).

- `onchange` — (index) on user/component-initiated changes

## API

```js
import { ref } from 'lemonadejs';
const wheel = ref();
html`<${Wheel} ref="${wheel}" />`;
// wheel.current.getIndex(...)  ·  wheel.current.setIndex(...)  ·  wheel.current.getValue(...)
```

- `getIndex()`
- `setIndex()`
- `getValue()`

## Styling

All classes follow the `lm-wheel-*` convention; visual variants are `data-*`
attributes on the root. Override freely — there is no styling engine to fight.

## Contract

The machine-readable schema ships with the package:

```js
import contract from '@lemonadejs/wheel/contract.json';
```

`verify.json` carries the conformance proof produced by `verify(Wheel)`.

Looking for the v5 plugin? See the [archived v5 documentation](/docs/v5/plugins/).