A lightweight, customizable iOS-style rolling wheel time picker component for your next React app — supports 12/24-hour format, infinite scroll, and multiple locales.
Check out the live demo here: Live Demo
npm i react-ios-style-time-picker
import { useState } from 'react';
import { TimePicker } from 'react-ios-style-time-picker';
import 'react-ios-style-time-picker/style.css';
function App() {
const [time, setTime] = useState<{ hour: number; minute: number }>({
hour: new Date().getHours(),
minute: new Date().getMinutes(),
});
const handleTimeChange = (hour: number, minute: number) => {
setTime({ hour, minute });
};
return (
<div>
<TimePicker onChange={handleTimeChange} hourFormat='12' />
</div>
);
}import { useState } from 'react';
import { TimePicker } from 'react-ios-style-time-picker';
import 'react-ios-style-time-picker/style.css';
function App() {
const [time, setTime] = useState<{ hour: number; minute: number }>({
hour: new Date().getHours(),
minute: new Date().getMinutes(),
});
const handleTimeChange = (hour: number, minute: number) => {
setTime({ hour, minute });
};
return (
<div>
<TimePicker onChange={handleTimeChange} hourFormat='24' />
</div>
);
}Pass value and update it from onChange, like a React <input>. Changing value
from outside (buttons, API responses, resets) moves the wheels without recreating
the picker and without calling onChange.
import { useState } from 'react';
import { TimePicker, type TimePickerValue } from 'react-ios-style-time-picker';
import 'react-ios-style-time-picker/style.css';
function App() {
const [time, setTime] = useState<TimePickerValue>({ hour: 9, minute: 0 });
return (
<div>
<TimePicker
value={time}
onChange={(hour, minute) => setTime({ hour, minute })}
/>
<button onClick={() => setTime({ hour: 18, minute: 30 })}>6:30 PM</button>
</div>
);
}<TimePicker
defaultValue={{ hour: 7, minute: 30 }}
onChange={(hour, minute) => console.log(hour, minute)}
/>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
onChange |
(hour: number, minute: number) => void |
✅ | - | Called (debounced 100ms) when the user changes the time. hour is always 0-23 |
value |
{ hour: number; minute: number } |
❌ | undefined |
Controlled time (hour 0-23, minute 0-59). Changes move the wheels without firing onChange |
defaultValue |
{ hour: number; minute: number } |
❌ | current time | Initial time for uncontrolled usage. Ignored when value is set |
initTime |
Date |
❌ | new Date() |
Deprecated — alias of defaultValue, kept for backward compatibility |
infinite |
boolean |
❌ | false |
Enables infinite scroll style |
className |
string |
❌ | undefined |
Custom class name for styling |
hourFormat |
'12' | '24' |
❌ | '12' |
Time format (12-hour/24-hour) |
locale |
'en' | 'ko' | 'ja' | 'zh' |
❌ | 'en' |
Language for AM/PM (English, Korean, Japanese, Chinese). Changes are applied in place |
Exported types: TimePickerProps, TimePickerValue, TimePickerLocale.
- Initial value priority:
value→defaultValue→initTime→ current time.defaultValue/initTimeare read on mount only. onChangeon mount: in uncontrolled modeonChangeis called once after mount with the initial time (same as previous versions). In controlled mode it is not.- Controlled mode: user gestures move the wheels immediately and report through
onChange; updatevaluewith the reported time. Settingvalueto the time the picker already shows is a no-op, so echoingonChangeback never loops. If you do not updatevalue, the wheels keep the user's selection untilvaluechanges. Avaluechange during a drag is overridden when the drag ends. Invalid values are ignored with a console warning. - Rebuilds: changing
hourFormatorinfinitere-renders the wheels (their DOM structure differs) and keeps the currently selected time.localeonly swaps the AM/PM labels and does not rebuild.
initTime still works but is deprecated and may be removed in a future major version.
- <TimePicker initTime={new Date(2025, 0, 1, 7, 30)} onChange={handleChange} />
+ <TimePicker defaultValue={{ hour: 7, minute: 30 }} onChange={handleChange} />While a wheel moves, only items that enter or leave the visible range have their
visibility updated, instead of every item on every animation frame. For a 60-item
minute wheel, a 60-frame flick went from 3,600 to 60 style.visibility writes.
See BENCHMARK.md for the method, all scenarios and how to reproduce.
12: Displays AM/PM notation24: Displays 0-23 hour format
We appreciate your feedback and contributions. If you have feature requests, questions, or want to contribute code or config files, please don't hesitate to use the GitHub Issue tracker.
We welcome all individual contributors, regardless of their level of experience or skill set. Your contributions are valuable, and we are excited to see what you can accomplish in this collaborative and supportive environment.
Inspired by ios-style-picker
It's forked from this gist
The MIT License.


