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.
- π‘ 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
npm install @poursha98/react-ios-time-pickerRequirements: Tailwind CSS v3.0+ must be installed.
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)}
/>
);
}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 />;
}Enable infinite looping for wheels (except AM/PM):
<TimePicker value={time} onChange={setTime} loop />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>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"
/>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>
);
}<TimePickerRoot value={time} onChange={setTime} is12Hour>
<TimePickerWheels>
<TimePickerWheel type="hour" />
<TimePickerSeparator>:</TimePickerSeparator>
<TimePickerWheel type="minute" />
<TimePickerWheel type="period" /> {/* AM/PM wheel */}
</TimePickerWheels>
<TimePickerButton />
</TimePickerRoot>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}
/>
);
}| 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) |
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>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>;Container for the wheel columns.
<TimePickerWheels className="flex gap-2">
{/* wheels go here */}
</TimePickerWheels>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 */}The colon between wheels.
<TimePickerSeparator className="text-blue-500">:</TimePickerSeparator>Labels for each wheel column.
<TimePickerLabel type="hour">Hour</TimePickerLabel>
<TimePickerLabel type="minute">Minute</TimePickerLabel>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>;| 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 |
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);
}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.
<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><TimePicker
value={time}
onChange={setTime}
numerals="fa" // Persian numerals + Persian text
/>// 15-minute intervals
<TimePicker
value={time}
onChange={setTime}
minuteStep={15}
/>
// Or custom array
<TimePicker
value={time}
onChange={setTime}
minutes={[0, 15, 30, 45]}
/>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>
);
}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>
);
}- 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
- Chrome, Edge, Safari, Firefox (latest 2 versions)
- iOS Safari 13+
- Android Chrome 80+
MIT Β© Poursha98


