Smooth, 120 FPS infinite looping time picker for React Native & Expo with zero external dependencies.
- π Infinite Looping Runway: Seamless
55 -> 00and23 -> 00rolling without jarring programmatic jumps or edge collisions. - β‘ 120 FPS Native Motion: Pure native Animated driver with continuous scaling (
44pt -> 34pt -> 26pt) and opacity cross-fade. - π‘οΈ Zero Native Node Leaks: Memoized row interpolation nodes created once on mount, preventing native driver memory leaks and scroll degradation ("tired wheels").
- π¦ Zero External Dependencies: Built exclusively on core React Native
AnimatedandScrollView. Works seamlessly in Expo Go, Bare React Native, and React Native for Web without pod installs or Reanimated version conflicts. - π 12-Hour & 24-Hour Modes: 24h continuous clock or 12h with an AM/PM wheel.
- π¨ Dark / Light & Custom Theming: Out-of-the-box dark and light modes, customizable colors, font families, and item heights.
- πͺ Inline & Modal Support: Use directly in your screen layout or as a pop-up modal.
# npm
npm install react-native-loop-time-picker
# yarn
yarn add react-native-loop-time-picker
# pnpm
pnpm add react-native-loop-time-picker
# expo
npx expo install react-native-loop-time-pickerNo native pods or Gradle configuration required!
import React, { useState } from 'react';
import { View } from 'react-native';
import { TimePicker } from 'react-native-loop-time-picker';
export default function App() {
const [hour, setHour] = useState(14);
const [minute, setMinute] = useState(30);
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<TimePicker
hour={hour}
minute={minute}
onTimeChange={(h, m) => {
setHour(h);
setMinute(m);
}}
is24Hour={true}
minuteInterval={5}
theme="dark" // or "light"
title="Select Reminder Time"
/>
</View>
);
}import React, { useState } from 'react';
import { Button, View } from 'react-native';
import { TimePickerModal } from 'react-native-loop-time-picker';
export default function ModalExample() {
const [hour, setHour] = useState(9);
const [minute, setMinute] = useState(0);
const [isOpen, setIsOpen] = useState(false);
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Button title="Open Time Picker" onPress={() => setIsOpen(true)} />
<TimePickerModal
visible={isOpen}
hour={hour}
minute={minute}
is24Hour={false} // 12-hour AM/PM mode
minuteInterval={5}
theme="dark"
onConfirm={(newHour, newMinute) => {
setHour(newHour);
setMinute(newMinute);
setIsOpen(false);
}}
onCancel={() => setIsOpen(false)}
confirmText="Save"
cancelText="Cancel"
/>
</View>
);
}If you've ever tried to build an infinite loop picker in React Native, you know how deceitfully hard it is. Almost every existing approach suffers from critical flaws:
- The Instant Jump Trap (
scrollTo({ animated: false })): Attempting to silently jump back to the middle of a list while momentum is active causes values to bounce wildly, numbers to freeze small between rows, and laggy headers. - Virtualization Flash (
FlatList): Virtualized lists cannot mount cells fast enough at high swipe velocities (10,000+ px/sec), creating blank white holes. - The Native Driver Memory Leak ("Tired Wheels"): Calling
scrollY.interpolate(...)inside list renders attaches hundreds of zombie native interpolation nodes to the native UI driver on every frame. Within a few seconds of spinning, the UI thread bogs down and drops to 15 FPS.
react-native-loop-time-picker solves this permanently:
- Continuous Runway (
LOOP_COPIES = 11): Instead of risky programmatic jumps during scroll, the wheel uses 11 repeating copies initialized at center (copy 5). Users get 5 full revolutions (>3,300 points of scroll / 60 items) in both directions. NativedecelerationRate="fast"halts any swipe in 4β8 rows, making boundary collisions virtually impossible without requiring any jump hacks. - Memoized Native Nodes: Each row (
WheelRow) is an isolated memoized component. Interpolation nodes are created once on mount per row and reused for the component lifetime: zero memory leaks, zero frame drops, consistent 120 FPS. - Graceful Snapping (
findNearestIndex): If an unrounded minute (e.g.23) is passed into a 5-minute interval picker, it smoothly maps to the nearest interval (25) without jumping back to 0.
| Prop | Type | Default | Description |
|---|---|---|---|
hour |
number |
Required | Current hour in 24-hour format (0β23). |
minute |
number |
Required | Current minute (0β59). |
onTimeChange |
(hour: number, minute: number) => void |
Required | Callback fired when hour or minute changes. |
is24Hour |
boolean |
true |
When false, displays 12-hour clock (1β12) with an AM/PM selector. |
minuteInterval |
number |
5 |
Step interval for minutes (e.g., 1, 5, 10, 15). |
loop |
boolean |
true |
Whether the wheels loop infinitely. |
theme |
'light' | 'dark' | TimePickerTheme |
'light' |
Preset theme name or custom theme object. |
title |
string |
undefined |
Optional title displayed above the live time readout. |
showPreview |
boolean |
true |
Whether to show the header with live preview. |
style |
ViewStyle |
undefined |
Custom container style. |
testID |
string |
undefined |
Test identifier. |
Inherits all <TimePicker /> props plus:
| Prop | Type | Default | Description |
|---|---|---|---|
visible |
boolean |
Required | Controls modal visibility. |
onConfirm |
(hour: number, minute: number) => void |
Required | Fired when user presses the confirm button. |
onCancel |
() => void |
Required | Fired when user dismisses the modal. |
confirmText |
string |
'Confirm' |
Text for the primary confirm button. |
cancelText |
string |
'Cancel' |
Text for the cancel button. |
animationType |
'none' | 'slide' | 'fade' |
'fade' |
Modal transition animation. |
You can pass a custom theme object to match your design system:
<TimePicker
hour={hour}
minute={minute}
onTimeChange={(h, m) => { setHour(h); setMinute(m); }}
theme={{
cardBackgroundColor: '#121212',
activeTextColor: '#FFFFFF',
inactiveTextColor: '#666666',
highlightColor: '#262626',
separatorColor: '#FFFFFF',
fontFamily: 'Inter-Bold',
}}
/>Run unit tests verifying boundary continuity, 12h/24h conversions, and runway calculations:
npm testMIT Β© Kirill Zolygin
