Wheel Picker

iOS-style wheel picker with a 3D barrel, flick inertia, detent snapping, and a fluid selection morph.

Installation

npx shadcn@latest add @iconiq/wheel-picker

File Structure

Usage

"use client";

import { useState } from "react";
import { WheelPicker, WheelPickerColumn } from "@/components/ui/wheel-picker";

const MONTHS = [
  "January", "February", "March", "April", "May", "June", "July",
  "August", "September", "October", "November", "December",
];
const YEARS = Array.from({ length: 106 }, (_, i) => String(1945 + i));

export function DateWheelPicker() {
  const [month, setMonth] = useState("October");
  const [day, setDay] = useState("9");
  const [year, setYear] = useState("2037");

  const dayCount = new Date(
    Number(year),
    MONTHS.indexOf(month) + 1,
    0
  ).getDate();
  const days = Array.from({ length: dayCount }, (_, i) => String(i + 1));

  return (
    <div className="w-full max-w-[340px]">
      <WheelPicker aria-label="Date">
        <WheelPickerColumn
          aria-label="Month"
          className="flex-[1.6]"
          loop
          onChange={setMonth}
          options={MONTHS}
          value={month}
        />
        <WheelPickerColumn
          aria-label="Day"
          loop
          onChange={setDay}
          options={days}
          value={days.includes(day) ? day : days[days.length - 1]}
        />
        <WheelPickerColumn
          aria-label="Year"
          onChange={setYear}
          options={YEARS}
          value={year}
        />
      </WheelPicker>
    </div>
  );
}

Props

Props
Description

WheelPicker

children

One or more WheelPickerColumn parts laid out side by side over the shared lens.

Type ReactNode

visibleCount

Odd number of rows visible on the barrel. Also sets the curvature — more rows means a deeper cylinder.

Type 3 | 5 | 7·Default 5

itemHeight

Row height in px shared by every column, the detent grid, and the lens.

Type number·Default 44

lens

Renders a rounded lens segment behind each column's selected row, like the iOS picker highlight.

Type boolean·Default true

aria-label

Accessible label for the picker group.

Type string

className

Merged onto the root shell for width, padding, or card chrome overrides.

Type string

WheelPickerColumn

options

Rows to render. Each option is a plain string or { value, label?, disabled? }. Disabled rows are skipped when snapping.

Type WheelPickerOption[]

value

Controlled selected value. External changes animate the wheel to the matching row.

Type string

defaultValue

Starting value for uncontrolled usage. Falls back to the first enabled option.

Type string

onChange

Fires as the wheel ticks past each detent while scrolling, matching iOS picker behavior.

Type (value: string, option: { value; label }) => void

onValueCommit

Fires once the wheel settles on a detent after a drag, flick, scroll, tap, or key press ends.

Type (value: string, option: { value; label }) => void

loop

Wraps the column infinitely, like iOS hours and minutes. Columns shorter than visibleCount + 2 fall back to bounded scrolling automatically.

Type boolean·Default false

disabled

Disables interaction and lowers the column opacity.

Type boolean·Default false

name

Hidden input name for native form submission.

Type string

aria-label

Accessible label for the column listbox.

Type string

id

Root element id used for the aria-activedescendant option wiring.

Type string

className

Merged onto the column for width or flex overrides — columns are flex-1 by default.

Type string

Registry bundle

Install the exact registry entry shown on the right when you want the component file and its declared runtime dependencies together.

Dependencies: motion.

This is a pure Motion component with no Radix UI or Base UI primitive underneath — the barrel, inertia, and detent physics are custom.

The generated registry file is /r/wheel-picker.json.

Contact

Additionally, if you find any bug or issue, feel free to raise an issue.

January
July
August
September
October
November
December
6
7
8
9
10
11
12
2034
2035
2036
2037
2038
2039
2040