@poursha98/react-ios-time-picker
v2.0.3
Published
iOS-style wheel picker component for React with smooth scroll-snap, accessibility, 12-hour AM/PM support, and shadcn-style compound components for full customization.
Maintainers
Readme
react-ios-time-picker
A beautiful, accessible iOS-style time picker for React with Tailwind CSS and shadcn-style compound components. Styled with Tailwind out of the box—no CSS import needed. Features smooth scroll-snap physics, 12-hour AM/PM support, touch/mouse drag, RTL support, and full TypeScript types.
✨ Features
- 🎡 iOS-style scroll physics - Native scroll-snap behavior
- ♾️ Infinite Scrolling - Seamless looping of wheels (optional)
- 🧩 Compound components - Mix and match parts like shadcn/ui
- 🕐 12-hour AM/PM support - Built-in period wheel
- 🖱️ Multi-input support - Touch, mouse drag, and keyboard
- ♿ Accessible - ARIA listbox pattern with full keyboard support
- 🌐 RTL & Persian numerals - Built-in support for Persian/Arabic
- 🎨 Tailwind-first - Styled with Tailwind CSS, no CSS import needed
- 🎨 Fully customizable - Override styles easily via
classNameprop - 📦 Lightweight - ~5KB gzipped
- 🔧 TypeScript - Complete type definitions included
Note: Requires Tailwind CSS v3.0+ in your project
📦 Installation
npm install @poursha98/react-ios-time-pickerRequirements: Tailwind CSS v3.0+ must be installed.
🚀 Quick Start
Simple Usage (All-in-One)

import { useState } from "react";
import { TimePicker } from "@poursha98/react-ios-time-picker";
// No CSS import needed! ✨
function App() {
const [time, setTime] = useState("09:30");
return (
<TimePicker
value={time}
onChange={setTime}
onConfirm={() => console.log("Selected:", time)}
/>
);
}12-Hour Format with AM/PM

import { useState } from "react";
import { TimePicker } from "@poursha98/react-ios-time-picker";
function App() {
const [time, setTime] = useState("02:30 PM");
return <TimePicker value={time} onChange={setTime} is12Hour />;
}Infinite Scrolling (Loop)
Enable infinite looping for wheels (except AM/PM):
<TimePicker value={time} onChange={setTime} loop />Customizing Styles
Easily override default styles with your own Tailwind classes:
<TimePicker
value={time}
onChange={setTime}
className="bg-slate-900 p-8 rounded-3xl shadow-2xl" // Override container
/>Or use compound components for granular control:
<TimePickerRoot
value={time}
onChange={setTime}
className="bg-linear-to-br from-purple-500 to-pink-500 p-8"
>
<TimePickerTitle className="text-white text-2xl">Pick a Time</TimePickerTitle>
<TimePickerWheels>
<TimePickerWheel type="hour" className="bg-white/20 backdrop-blur" />
<TimePickerSeparator className="text-white">:</TimePickerSeparator>
<TimePickerWheel type="minute" className="bg-white/20 backdrop-blur" />
</TimePickerWheels>
<TimePickerButton className="bg-white text-purple-600 hover:bg-gray-100">
Confirm
</TimePickerButton>
</TimePickerRoot>Customizing Wheel Item Colors
Customize the colors of wheel items using the classNames prop:
<TimePickerWheel
type="hour"
classNames={{
item: "text-gray-400", // Unselected items
selectedItem: "text-primary", // Selected item
}}
/>You can also use Tailwind's arbitrary variant syntax to target data attributes:
<TimePickerWheel
type="minute"
className="[&_[data-wheel-item]]:text-gray-400 [&_[data-wheel-item][data-selected]]:text-blue-500"
/>Compound Components (Full Control)

For maximum flexibility, use individual compound components:
import { useState } from "react";
import {
TimePickerRoot,
TimePickerTitle,
TimePickerWheels,
TimePickerWheel,
TimePickerSeparator,
TimePickerButton,
} from "@poursha98/react-ios-time-picker";
function CustomTimePicker() {
const [time, setTime] = useState("09:30");
return (
<TimePickerRoot
value={time}
onChange={setTime}
className="bg-slate-900 rounded-2xl p-6"
>
<TimePickerTitle className="text-white text-xl font-bold mb-4">
⏰ Select Time
</TimePickerTitle>
<TimePickerWheels className="flex justify-center items-center gap-2">
<TimePickerWheel type="hour" className="bg-slate-800 rounded-lg" />
<TimePickerSeparator className="text-blue-400 text-2xl font-bold">
:
</TimePickerSeparator>
<TimePickerWheel type="minute" className="bg-slate-800 rounded-lg" />
</TimePickerWheels>
<TimePickerButton className="mt-6 w-full bg-blue-500 text-white py-3 rounded-xl">
Confirm Selection
</TimePickerButton>
</TimePickerRoot>
);
}12-Hour with Compound Components
<TimePickerRoot value={time} onChange={setTime} is12Hour>
<TimePickerWheels>
<TimePickerWheel type="hour" />
<TimePickerSeparator>:</TimePickerSeparator>
<TimePickerWheel type="minute" />
<TimePickerWheel type="period" /> {/* AM/PM wheel */}
</TimePickerWheels>
<TimePickerButton />
</TimePickerRoot>Low-Level Wheel (Custom Pickers)
Build completely custom pickers using the base Wheel component:
import { useState } from "react";
import { Wheel } from "@poursha98/react-ios-time-picker";
const fruits = [
"🍎 Apple",
"🍊 Orange",
"🍋 Lemon",
"🍇 Grape",
"🍓 Strawberry",
];
function FruitPicker() {
const [selected, setSelected] = useState(0);
return (
<Wheel
items={fruits}
value={selected}
onChange={setSelected}
itemHeight={48}
visibleCount={5}
/>
);
}📚 API Reference
TimePicker Props (All-in-One)
| Prop | Type | Default | Description |
| ------------------- | ------------------------ | -------------------- | ----------------------------------------- |
| value | string | required | Time value ("HH:MM" or "HH:MM AM/PM") |
| onChange | (time: string) => void | required | Called when time changes |
| onConfirm | () => void | - | Called when confirm button is clicked |
| is12Hour | boolean | false | Enable 12-hour format with AM/PM |
| numerals | "en" \| "fa" \| "auto" | "auto" | Number format and text language |
| hours | number[] | [0-23] or [1-12] | Custom hours array |
| minutes | number[] | [0-59] | Custom minutes array |
| minuteStep | number | - | Minute interval (5, 15, 30) |
| showTitle | boolean | true | Show title |
| showLabels | boolean | true | Show hour/minute labels |
| showConfirmButton | boolean | true | Show confirm button |
| itemHeight | number | 48 | Height of each wheel item |
| visibleCount | number | 5 | Number of visible items |
| loop | boolean | false | Enable infinite looping |
| disabled | boolean | false | Disable the picker |
| className | string | - | Root element className |
| classNames | TimePickerClassNames | - | CSS class names (legacy) |
| styles | TimePickerStyles | - | Inline styles (legacy) |
Compound Components
TimePickerRoot
The context provider that wraps all other components.
<TimePickerRoot
value={time}
onChange={setTime}
is12Hour={false}
numerals="auto"
disabled={false}
loop={false}
onConfirm={() => {}}
className="my-picker"
>
{children}
</TimePickerRoot>TimePickerTitle
Displays the picker title. Uses <h2> by default.
<TimePickerTitle className="text-xl font-bold">Select Time</TimePickerTitle>;
{
/* Use asChild for custom elements */
}
<TimePickerTitle asChild>
<h1>Choose Time</h1>
</TimePickerTitle>;TimePickerWheels
Container for the wheel columns.
<TimePickerWheels className="flex gap-2">
{/* wheels go here */}
</TimePickerWheels>TimePickerWheel
Individual wheel for hour, minute, or period (AM/PM).
<TimePickerWheel type="hour" className="w-20" />
<TimePickerWheel type="minute" className="w-20" />
<TimePickerWheel type="period" className="w-16" /> {/* AM/PM */}TimePickerSeparator
The colon between wheels.
<TimePickerSeparator className="text-blue-500">:</TimePickerSeparator>TimePickerLabel
Labels for each wheel column.
<TimePickerLabel type="hour">Hour</TimePickerLabel>
<TimePickerLabel type="minute">Minute</TimePickerLabel>TimePickerButton
Confirm button that triggers onConfirm.
<TimePickerButton className="bg-blue-500 text-white px-4 py-2 rounded">
Confirm
</TimePickerButton>;
{
/* Use asChild for custom elements */
}
<TimePickerButton asChild>
<a href="/next">Continue</a>
</TimePickerButton>;Wheel Props
| Prop | Type | Default | Description |
| -------------- | ---------------------------------------- | ------------ | ----------------------------- |
| items | T[] | required | Array of items to display |
| value | number | required | Currently selected index |
| onChange | (index: number) => void | required | Called when selection changes |
| itemHeight | number | 40 | Height of each item in pixels |
| visibleCount | number | 5 | Number of visible items |
| width | string \| number | "100%" | Width of the wheel |
| loop | boolean | false | Enable infinite looping |
| renderItem | (item, index, isSelected) => ReactNode | - | Custom item renderer |
| disabled | boolean | false | Disable the wheel |
| classNames | WheelClassNames | - | CSS class names |
| styles | WheelStyles | - | Inline styles |
| aria-label | string | - | Accessible label |
| getItemLabel | (item, index) => string | - | Accessible item labels |
🎨 Styling
CSS Variables
Customize colors using CSS variables:
:root {
--time-picker-bg: #ffffff;
--time-picker-text: #1f2937;
--time-picker-text-secondary: #9ca3af;
--time-picker-primary: #3b82f6;
--time-picker-primary-light: rgba(59, 130, 246, 0.1);
}
/* Dark mode */
.dark {
--time-picker-bg: #1f2937;
--time-picker-text: #f3f4f6;
--time-picker-text-secondary: #9ca3af;
--time-picker-primary: #60a5fa;
--time-picker-primary-light: rgba(96, 165, 250, 0.1);
}Data Attribute Selectors
Style using data attributes for more specificity:
/* Root */
[data-time-picker] {
background: #1e293b;
}
/* Title */
[data-time-picker-title] {
color: white;
}
/* Wheel items - no default colors, customize as needed */
[data-wheel-item] {
color: #94a3b8; /* Unselected items */
}
[data-wheel-item][data-selected] {
color: #3b82f6; /* Selected item */
font-weight: bold;
}
/* Wheel indicator */
[data-wheel-indicator] {
border-color: #3b82f6;
}Note: As of version 2.0, wheel items no longer have hardcoded text colors. You must explicitly set colors using the classNames prop, data attribute selectors, or the className prop on TimePickerWheel.
Tailwind CSS Example
<TimePickerRoot
value={time}
onChange={setTime}
className="bg-slate-900 rounded-2xl p-6 shadow-xl"
>
<TimePickerTitle className="text-white font-bold text-xl mb-4">
Choose Time
</TimePickerTitle>
<TimePickerWheels className="flex justify-center gap-2">
<TimePickerWheel type="hour" className="bg-slate-800 rounded-lg" />
<TimePickerSeparator className="text-blue-400 text-2xl">
:
</TimePickerSeparator>
<TimePickerWheel type="minute" className="bg-slate-800 rounded-lg" />
</TimePickerWheels>
<TimePickerButton className="mt-4 w-full bg-linear-to-r from-blue-500 to-purple-500 text-white py-3 rounded-xl hover:opacity-90 transition">
Confirm
</TimePickerButton>
</TimePickerRoot>📝 Examples
Persian/RTL Support
<TimePicker
value={time}
onChange={setTime}
numerals="fa" // Persian numerals + Persian text
/>Custom Minute Steps
// 15-minute intervals
<TimePicker
value={time}
onChange={setTime}
minuteStep={15}
/>
// Or custom array
<TimePicker
value={time}
onChange={setTime}
minutes={[0, 15, 30, 45]}
/>React Hook Form Integration
import { Controller, useForm } from "react-hook-form";
import { TimePicker } from "@poursha98/react-ios-time-picker";
function MyForm() {
const { control, handleSubmit } = useForm({
defaultValues: { appointmentTime: "09:00" },
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<Controller
name="appointmentTime"
control={control}
render={({ field }) => (
<TimePicker
value={field.value}
onChange={field.onChange}
showConfirmButton={false}
/>
)}
/>
<button type="submit">Book Appointment</button>
</form>
);
}Without Default Styles
Import only the components without any CSS:
import {
TimePickerRoot,
TimePickerWheel,
TimePickerButton,
} from "@poursha98/react-ios-time-picker";
// No CSS import!
function MinimalPicker() {
const [time, setTime] = useState("09:30");
return (
<TimePickerRoot value={time} onChange={setTime}>
<TimePickerWheel type="hour" />
<span>:</span>
<TimePickerWheel type="minute" />
<TimePickerButton>OK</TimePickerButton>
</TimePickerRoot>
);
}♿ Accessibility
- Full keyboard navigation: Arrow keys, Home, End, Page Up/Down
- ARIA
listboxpattern withoptionroles - Screen reader announcements via
aria-labelandgetItemLabel - Focus management and visible focus indicators
- Unique ARIA IDs for multiple instances
🌐 Browser Support
- Chrome, Edge, Safari, Firefox (latest 2 versions)
- iOS Safari 13+
- Android Chrome 80+
📄 License
MIT © Poursha98
