WingoUI
ComponentsTemplatesDocsChangelogBlogAI agents
ThemePricing
  1. Home
  2. Blog
  3. Mobile UI in React
  4. React Wheel Picker: iOS Wheels for Times and Dates
  1. Blog
  2. Mobile UI in React
  3. React Wheel Picker: iOS Wheels for Times and Dates
Mobile UI in React
Mobile UI in React

React Wheel Picker: iOS Wheels for Times and Dates

SR
Serban Rusu · Founder of Wingo UI
Oct 9, 2026 · 8 min read

On this page

0%
  1. When is a wheel picker the right control?
  2. Why build a wheel on a native snapping scroller?
  3. How do mouse and keyboard users turn the wheel?
  4. What props does the Picker Wheel need?
  5. How do you build a React time picker wheel that still types on desktop?
  6. Should a date be a wheel or a calendar?
  7. What goes wrong with a wheel picker on a phone?
  8. Where should you start?
  9. FAQ
    1. Is there an iOS-style wheel picker for React web apps?
    2. Should I use input type=time instead of a wheel picker?
    3. How do I make a wheel picker loop around in React?
    4. Does a web wheel picker vibrate on iPhone?
    5. Is the Wingo UI Picker Wheel free?

TL;DR

A React wheel picker that feels native lets the browser do the scrolling: a scroll-snap list brings the phone's own momentum and rubber band, each row is turned on a 3D cylinder from scrollTop in one animation frame, and the value commits once, when the wheel comes to rest. Use wheels for times, durations and birth dates on phones, keep a typed field on desktop, and give mouse drag, the scroll wheel and the keyboard their own paths. Wingo UI's Pro Picker Wheel and Time Picker ship all of this, with a bottom sheet below 768px.

Published Oct 9, 2026

You want a React wheel picker because a phone user should not type 14:30 on a keyboard that covers half the form, and paging a calendar back 36 years to a birth date is worse. The hard part is the feel: the React wheel packages we checked replace the browser's scrolling with their own touch physics. This guide builds the wheel on a native snapping scroller with rows turned on a 3D cylinder, then covers mouse and keyboard paths, the time and date fields built on it, and the pitfalls. How wheel pickers compare across libraries is in our best React component library for mobile comparison.

When is a wheel picker the right control?

When the list is medium to long, ordered, and the user can guess what is hidden: hours, minutes, durations, quantities, a birth year. Apple's Human Interface Guidelines for pickers (opens in a new tab) say to "consider using a picker to offer medium-to-long lists of items", to prefer a pull-down button for "a fairly short list of choices", and a list or table for "a very large set of items". It also suggests minute steps that divide 60 evenly, such as quarter hours.

On the web that gives four rules:

  • Two to five options: a Segmented Control, so every option is visible.
  • 12 hours, 60 minutes, 1 to 99 items, the years since 1920: a wheel.
  • Hundreds of unordered names (countries, products): a searchable Select. A wheel only jumps to a typed prefix.
  • A date near today (a delivery, a booking): a calendar, because the weekday matters.

Why build a wheel on a native snapping scroller?

Because then the finger, the momentum and the rubber band at the ends are the phone's own, and a trackpad or a mouse wheel turns it with no extra code. A search for "ios picker web" in October 2026 mixed a Mendix widget, React Native packages and Flutter's CupertinoPicker in with the web answers. The two React DOM packages we found both run their own physics:

Package (as of October 2026)License, last releaseHow it moves
@ncdai/react-wheel-picker (opens in a new tab) 1.2.3MIT, August 23, 2026own touch, mouse and wheel handlers; rows on a 3D cylinder with CSS transforms
react-mobile-picker (opens in a new tab) 1.2.0MIT, August 3, 2025own touchmove handler and translate3d; mouse wheel off by default

Both work. @ncdai/react-wheel-picker, which also handles arrows, Home / End and type-ahead, is the better pick if you want a free, unstyled wheel and will build the field yourself. Its feel is tuned through dragSensitivity and scrollSensitivity; a native scroller decelerates like every other list on that phone.

The native recipe has three parts. The list scrolls with scroll-snap-type: y mandatory, every row snaps to the center, and two rows of padding let the first and last rows reach it. One requestAnimationFrame per scroll reads scrollTop and writes rotateX and opacity straight to the rows, never through React state. And the value commits once, at rest: on scrollend, which MDN (opens in a new tab) lists from Chrome 114, Firefox 109 and Safari 26.2, with a 120ms idle timer for older Safari.

tsx
"use client";
import { useEffect, useRef } from "react";
const ROW = 44; // five rows in a 220px window
export function MiniWheel({ options, onSettle }: { options: string[]; onSettle: (value: string) => void }) {
const listRef = useRef<HTMLDivElement>(null);
const settleRef = useRef(onSettle);
useEffect(() => {
settleRef.current = onSettle;
});
useEffect(() => {
const list = listRef.current;
if (!list) return;
const rows = Array.from(list.firstElementChild?.children ?? []) as HTMLElement[];
let frame = 0;
let idle: ReturnType<typeof setTimeout> | undefined;
let last = "";
// the turn and the fade go straight to the rows, never through React state
const paint = () => {
frame = 0;
const pos = list.scrollTop / ROW;
rows.forEach((row, i) => {
const d = i - pos;
row.style.transform = `perspective(400px) rotateX(${Math.max(-55, Math.min(55, -d * 20))}deg)`;
row.style.opacity = String(Math.max(0.35, 1 - Math.min(Math.abs(d), 3) * 0.22));
});
};
// one commit when the wheel is at rest, never one per row passed
const settle = () => {
clearTimeout(idle);
const value = options[Math.round(list.scrollTop / ROW)];
if (value !== undefined && value !== last) settleRef.current((last = value));
};
const onScroll = () => {
if (!frame) frame = requestAnimationFrame(paint);
clearTimeout(idle);
idle = setTimeout(settle, 120); // Safari before 26.2 has no scrollend
};
paint();
list.addEventListener("scroll", onScroll, { passive: true });
list.addEventListener("scrollend", settle);
return () => {
list.removeEventListener("scroll", onScroll);
list.removeEventListener("scrollend", settle);
cancelAnimationFrame(frame);
clearTimeout(idle);
};
}, [options]);
return (
<div ref={listRef} className="h-[220px] snap-y snap-mandatory overflow-y-scroll overscroll-contain [scrollbar-width:none]">
<div className="py-[88px]">
{options.map((option) => (
<div key={option} className="flex h-11 snap-center items-center justify-center text-xl tabular-nums">
{option}
</div>
))}
</div>
</div>
);
}

That is the core of the Wingo UI Picker Wheel. The sketch misses a starting value, disabled rows, looping, every input that is not a finger, and wheels mounted while hidden.

How do mouse and keyboard users turn the wheel?

A mouse wheel and a trackpad scroll it natively; dragging and keys need their own code. In the Picker Wheel only a mouse goes through pointer events: past 8 pixels of movement, snapping turns off and scrollTop follows the pointer. On release, a motion inertia animation continues from the measured velocity, and its modifyTarget rounds the landing point to a row. A click on a visible row glides it to the center.

Each column is one Tab stop with role="listbox". ArrowUp and ArrowDown move a row, PageUp and PageDown five, Home and End jump to the ends, typing jumps to the next option that starts with the text ("19", then "199" on a year wheel), and ArrowLeft and ArrowRight change column. aria-activedescendant points at the centered option, which carries aria-selected. The focus ring hugs the center row, where the edge fade cannot hide it.

What props does the Picker Wheel need?

columns (or options for a single wheel), a value and onValueChange. Everything else has a default:

tsx
"use client";
import { useState } from "react";
import { PickerWheel, pickerRange } from "@/components/ui/picker-wheel";
const hours = pickerRange(0, 12);
const minutes = pickerRange(0, 59, { pad: 2 });
export function CallDuration() {
const [duration, setDuration] = useState(["1", "25"]);
return (
<PickerWheel
aria-label="Call duration"
value={duration}
// once per settle, with every column's value and the id of the column that moved
onValueChange={(value) => setDuration(value)}
columns={[
{ id: "hours", label: "h", options: hours, align: "end" },
{ id: "minutes", label: "min", options: minutes, align: "start", loop: true },
]}
/>
);
}

The props worth knowing:

  • onValueChange fires once per settle, never per row passed, so it is safe to save from it.
  • loop (on the wheel or per column) wraps 59 back to 00: the column renders extra copies and jumps silently to the middle one at rest. Only the middle copy has role="option".
  • size sets 36, 44 or 52px rows; visibleRows (odd, default 5) makes the wheel 220px tall at md; perspective tilts rows 20, 10 or 0 degrees each (ios, subtle, flat).
  • A disabled option is struck through, and a wheel that lands on one glides on to the nearest enabled row: pickerRange(36, 46, { disabled: (n) => n === 38 }) for a sold-out shoe size.
  • haptics plays a selection tick each time a finger, a mouse drag or the scroll wheel brings a new row to the center, never for keys. It works on Chrome for Android; why a scroll cannot tick on an iPhone is in our web vibration API guide.

PickerWheelField puts the same wheels in a form field: a bottom sheet with Done and Cancel below 768px, a popover that applies live on desktop.

How do you build a React time picker wheel that still types on desktop?

Use the Time Picker, which switches by width. Below 768px the field is read-only with inputMode="none", so no keyboard opens, and a tap opens a bottom sheet with hour and minute wheels. From 768px up it is a text input that reads "1430", "2:30pm" or "2p", steps minutes with ArrowUp and ArrowDown, and opens a panel of columns on click. The value is always "HH:mm" in 24 hours:

tsx
"use client";
import { useState } from "react";
import { TimePicker } from "@/components/ui/time-picker";
const lunchBreak = (time: string) => time >= "13:00" && time < "14:00";
export function PickupTime() {
const [time, setTime] = useState("11:30");
return (
<TimePicker
label="Pickup time"
name="pickup"
locale="en-US"
min="09:00"
max="18:00"
minuteStep={15}
disabledTimes={lunchBreak}
value={time}
onValueChange={setTime}
/>
);
}
Type 2:30pm or 1430 and press Enter, or click the field for the columns; at phone width a tap opens the wheels in a bottom sheet, and the lunch hour stays blocked either way
$ npx wingo-ui@latest add time-picker
ProTime Picker docs

The sheet edits a draft: Done commits it, and dragging the sheet away keeps the old time. Times outside min and max or blocked by disabledTimes are disabled rows on the wheels too.

Should a date be a wheel or a calendar?

A wheel for a date the user knows by heart and that sits far from today, like a birth date: three flicks instead of paging through years. A calendar for dates near today, where the weekday matters. That is why the Date Picker opens a calendar, in a bottom sheet on phones.

For the birth date, useDateWheel is a React date wheel picker in one hook: day, month and year columns in the locale's order (month, day, year for en-US), month names in that language, days that follow the month (February 29 only in leap years) and years between min and max. Values are ISO dates such as "1990-05-17".

Flick the month, the day or the year, drag them with a mouse, or Tab to the year and type 1985: the date and the age under the wheel update when it comes to rest
$ npx wingo-ui@latest add picker-wheel
ProPicker Wheel docs

For the wheel on phones and a typed field on desktop, render both and switch with CSS, so the server HTML matches the first paint (the mobile-first React components guide explains why a useIsMobile check would flash). One ISO string feeds both:

tsx
"use client";
import { useState } from "react";
import { DatePicker } from "@/components/ui/date-picker";
import { PickerWheelField, useDateWheel } from "@/components/ui/picker-wheel";
import { formatDate, fromISODate, toISODate } from "@/lib/date-utils";
export function BirthdayField({ initial }: { initial: string }) {
const [birthday, setBirthday] = useState(initial); // "1990-05-17"
const [today] = useState(() => new Date());
const wheel = useDateWheel({ value: birthday, onValueChange: setBirthday, locale: "en-US", min: "1920-01-01", max: toISODate(today) });
return (
<>
{/* phones: three wheels in a bottom sheet, Done to commit */}
<div className="md:hidden">
<PickerWheelField
label="Date of birth"
{...wheel}
formatValue={() => formatDate(fromISODate(birthday), "en-US", { dateStyle: "long" })}
/>
</div>
{/* 768px and up: type 05171990, or open the calendar */}
<div className="hidden md:block">
<DatePicker
label="Date of birth"
locale="en-US"
max={today}
clearable={false}
value={fromISODate(birthday)}
onValueChange={(date) => date && setBirthday(toISODate(date))}
/>
</div>
<input type="hidden" name="birthday" value={birthday} />
</>
);
}

One catch: useDateWheel always holds a date (today when you pass none), so the field is never empty. That suits editing a profile; a required date that starts blank belongs in the Date Picker.

What goes wrong with a wheel picker on a phone?

Most bugs come from the wheel's surroundings. The Picker Wheel handles these, and you will meet them if you build your own:

  • The sheet steals the drag. A wheel at its first row hands a downward drag to the drawer. The columns carry data-vaul-no-drag, and overscroll-behavior: contain keeps a hard flick from scrolling the page.
  • A wheel mounted while hidden. Inside a closed sheet the list has no height, so setting scrollTop does nothing. A ResizeObserver scrolls to the value once the list has a size.
  • Committing per row. Saving on every row that passes floods your API and fires validation mid-spin.
  • Reduced motion. With prefers-reduced-motion the rows stay flat and programmatic moves jump instead of gliding.
  • Feedback only in the tick. Desktop and iPhone users feel nothing, so the centered row and the field text must carry the value.

Where should you start?

Open this page on your phone and flick the two demos above. The Picker Wheel, Time Picker and Date Picker are part of Wingo UI Pro; with a Pro account signed in through npx wingo-ui@latest login, npx wingo-ui@latest add picker-wheel copies the component and everything it imports into your project. A React wheel picker component is one piece of a phone-friendly form; the other patterns are in Mobile UI in React.

Components in this post

  • Picker Wheel

    The iOS wheel picker for the web: columns that spin with native momentum and click into place on a 3D cylinder, inline or as a field.

    Pro
  • Time Picker

    A time-of-day field to type into or pick from columns, with 12 / 24 hours by locale, minute steps, limits and iOS wheels on phones.

    Pro
  • Date Picker

    A date field to type in your locale or pick from a calendar popover, with presets, limits and an optional time; a sheet on phones.

    Pro

FAQ

Is there an iOS-style wheel picker for React web apps?

Yes. As of October 2026 the open source options are @ncdai/react-wheel-picker (1.2.3, MIT) and react-mobile-picker (1.2.0, MIT), which both run their own touch physics. Wingo UI's Picker Wheel (Pro) scrolls natively and adds a form field that opens a bottom sheet on phones, a date hook and haptic ticks.

Should I use input type=time instead of a wheel picker?

Use the native input when its look and behavior are good enough, because it costs nothing and is accessible by default. You cannot style the picker it opens, it differs between browsers and OS versions, and it cannot show blocked times such as a lunch break, which is when a custom wheel earns its code.

How do I make a wheel picker loop around in React?

Render the options several times and jump back to the middle copy whenever the wheel comes to rest, which looks identical. In the Wingo UI Picker Wheel, loop on the wheel or on one column does this, with at least 40 rows on each side of the middle copy so a hard flick never reaches the end.

Does a web wheel picker vibrate on iPhone?

No. Safari has no Vibration API, and the iPhone's one web haptic, the tick of a native switch, plays only for a tap, never for a scroll. On Chrome for Android the Picker Wheel ticks each time a new row reaches the center.

Is the Wingo UI Picker Wheel free?

No. The Picker Wheel, Time Picker and Date Picker are Pro components; the Drawer and Field they build on are free. Pro costs $8 a month, $80 a year or $150 once.

  • Mobile UI
  • Wheel picker
  • Time picker
  • iOS
  • React

Share

SR

About the author

Serban Rusu

Founder of Wingo UI

Serban Rusu is the founder of Wingo UI. He builds the component library, its CLI and its MCP server, and writes about React interfaces that work well on phones and with AI coding agents.

More from Serban
Keep reading

Related posts

All posts
Mobile UI in React

Web Vibration API: Haptic Feedback on Android and iPhone

The Web Vibration API in 2026: navigator.vibrate on Android, the iPhone switch tick that iOS 26.5 limits to real taps, and a React helper that fails quietly.

SRSerban Rusu·Oct 9, 2026·11 min read
Comparisons and alternatives

Best React Component Library for Mobile in 2026: 9 Compared

The best React component library for mobile web apps in 2026: 9 libraries checked at 390px for bottom sheets, touch targets, safe areas and gestures.

SRSerban Rusu·Oct 9, 2026·12 min read
Component guides

React Form Components: Accessible Fields, Zod and Mobile

React form components that share one anatomy (label, hint, error, counter), plug into react-hook-form and zod, and behave right on a phone, with full code.

SRSerban Rusu·Oct 9, 2026·21 min read
Newsletter

Get new posts by email

New guides, tutorials and comparisons from the Wingo UI blog, sent when they are published.

No spam. Unsubscribe at any time.

WingoUI

Animated, configurable, mobile-first React components. Copy the source, make it yours, and let your coding agent build with it.

ComponentsTemplatesPricingBlogTheme

Component categories

  • Buttons & Actions
  • Inputs
  • Forms
  • Navigation
  • Overlays
  • Feedback
  • Data Display
  • Tables & Lists
  • Charts & Stats
  • Layout
  • Media
  • AI Kit
  • Text & Effects
  • Mobile
  • Commerce
  • Marketing Sections
  • Blocks
  • Hooks & Utilities
Wingo UI