iOS-style wheel picker with a 3D barrel, flick inertia, detent snapping, and a fluid selection morph.
npx shadcn@latest add @iconiq/wheel-picker"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>
);
}childrenOne or more WheelPickerColumn parts laid out side by side over the shared lens.
Type ReactNode
visibleCountOdd number of rows visible on the barrel. Also sets the curvature — more rows means a deeper cylinder.
Type 3 | 5 | 7·Default 5
itemHeightRow height in px shared by every column, the detent grid, and the lens.
Type number·Default 44
lensRenders a rounded lens segment behind each column's selected row, like the iOS picker highlight.
Type boolean·Default true
aria-labelAccessible label for the picker group.
Type string
classNameMerged onto the root shell for width, padding, or card chrome overrides.
Type string
optionsRows to render. Each option is a plain string or { value, label?, disabled? }. Disabled rows are skipped when snapping.
Type WheelPickerOption[]
valueControlled selected value. External changes animate the wheel to the matching row.
Type string
defaultValueStarting value for uncontrolled usage. Falls back to the first enabled option.
Type string
onChangeFires as the wheel ticks past each detent while scrolling, matching iOS picker behavior.
Type (value: string, option: { value; label }) => void
onValueCommitFires once the wheel settles on a detent after a drag, flick, scroll, tap, or key press ends.
Type (value: string, option: { value; label }) => void
loopWraps the column infinitely, like iOS hours and minutes. Columns shorter than visibleCount + 2 fall back to bounded scrolling automatically.
Type boolean·Default false
disabledDisables interaction and lowers the column opacity.
Type boolean·Default false
nameHidden input name for native form submission.
Type string
aria-labelAccessible label for the column listbox.
Type string
idRoot element id used for the aria-activedescendant option wiring.
Type string
classNameMerged onto the column for width or flex overrides — columns are flex-1 by default.
Type string
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.