Every React Native project eventually needs a drum/wheel picker.
You know the one — that satisfying iOS-style scrolling cylinder
for selecting dates, times, or any list of values.
I needed one for a real production app. After trying every existing
library, I found the same problems everywhere: dead repos, no New
Architecture support, JS-only implementations with fake scroll
physics, or missing features that every app actually needs.
So I built react-native-drum-picker from scratch. Here's what
it took and why it's different.
The core problem with existing solutions
The most popular wheel picker libraries (like
@quidone/react-native-wheel-picker) are built entirely in
JavaScript — they use a React Native ScrollView and simulate
snap behavior in JS.
This works. But it's not the same as native.
On Android, the real drum picker is a RecyclerView with
LinearSnapHelper. On iOS it's UIPickerView. These are
platform components that users have felt thousands of times.
The physics, the momentum, the micro-snap — it's all there
natively. You can't fully replicate that in JS.
react-native-drum-picker is built with:
- Native Kotlin (
RecyclerView+LinearSnapHelper) on Android - Native Swift (
UIPickerView) on iOS - React Native New Architecture (Fabric) — the only drum picker that supports it
What it can do
import DrumPicker, {
DateDrumPicker,
TimeDrumPicker,
withVirtualized,
usePickerGroup,
usePickerGroupChangedEffect,
} from 'react-native-drum-picker';
Basic usage
const [index, setIndex] = useState(0);
<DrumPicker
items={['January', 'February', 'March', ...]}
selectedIndex={index}
onChange={({ nativeEvent }) => setIndex(nativeEvent.index)}
/>
Date picker with constraints
<DateDrumPicker
mode="day-month-year"
minDate={{ day: 1, month: 1, year: 2020 }}
maxDate={{ day: 31, month: 12, year: 2030 }}
onChange={setDate}
/>
Custom item rendering — flags, icons, colors
const countries = [
{ label: 'Uzbekistan', value: 'UZ', flag: '🇺🇿' },
{ label: 'Russia', value: 'RU', flag: '🇷🇺' },
{ label: 'USA', value: 'US', flag: '🇺🇸' },
];
<DrumPicker
items={countries}
renderItem={({ item, isSelected }) => (
<View style={{ flexDirection: 'row', gap: 8 }}>
<Text style={{ fontSize: 24 }}>{item.flag}</Text>
<Text style={{
color: isSelected ? '#000' : '#999',
fontWeight: isSelected ? '600' : '400',
}}>
{item.label}
</Text>
</View>
)}
onChange={({ nativeEvent }) => setCountry(nativeEvent.value)}
/>
Synchronize multiple pickers with PickerGroup
const group = usePickerGroup();
// React when any picker in the group settles
usePickerGroupChangedEffect(group, ({ pickerName, index, value }) => {
console.log(`${pickerName} settled on ${value}`);
if (pickerName === 'month') {
updateDaysForMonth(index + 1);
}
});
// Live preview while scrolling
usePickerGroupChangingEffect(group, ({ pickerName, value }) => {
setPreview(`${pickerName}: ${value}`);
});
<View style={{ flexDirection: 'row' }}>
<DrumPicker
pickerGroup={group}
pickerName="month"
items={MONTHS}
onChange={() => {}}
/>
<DrumPicker
pickerGroup={group}
pickerName="day"
items={days}
onChange={() => {}}
/>
</View>
Programmatic control via ref
const dateRef = useRef<DateDrumPickerRef>(null);
const today = new Date();
<DateDrumPicker
ref={dateRef}
mode="day-month-year"
onChange={setDate}
/>
<Button
title="Today"
onPress={() => dateRef.current?.scrollToDate({
day: today.getDate(),
month: today.getMonth() + 1,
year: today.getFullYear(),
}, { animated: true })}
/>
10,000+ items with virtualization
const VirtualizedDrumPicker = withVirtualized(DrumPicker);
<VirtualizedDrumPicker
items={allCitiesInTheWorld} // 10,000+ items
windowSize={20}
selectedIndex={index}
onChange={({ nativeEvent }) => setIndex(nativeEvent.index)}
/>
Circular (infinite loop) scrolling
// 58 → 59 → 00 → 01
<DrumPicker
circular
items={minutes}
onChange={({ nativeEvent }) => setMinute(nativeEvent.index)}
/>
Live scroll events
<TimeDrumPicker
onValueChanging={({ nativeEvent }) => {
// Fires on every scroll tick — before the picker settles
setPreviewHour(nativeEvent.hour);
}}
onChange={({ nativeEvent }) => {
// Fires once after scroll stops
saveTime(nativeEvent);
}}
/>
Full feature list
| Feature | react-native-drum-picker | @quidone | react-native-wheel-pick |
|---|---|---|---|
| Native Kotlin (Android) | ✅ | ❌ | ❌ |
| Native Swift (iOS) | ✅ | ❌ | ❌ |
| New Architecture (Fabric) | ✅ | ❌ | ❌ |
onValueChanging live scroll events |
✅ | ✅ | ❌ |
renderItem custom rendering |
✅ | ❌ | ❌ |
circular infinite loop |
✅ | ❌ | ❌ |
withVirtualized for large lists |
✅ | ✅ | ❌ |
PickerGroup multi-picker sync |
✅ | ✅ | ❌ |
ref API (scrollToIndex, scrollToValue) |
✅ | ❌ | ❌ |
minDate / maxDate constraints |
✅ | ✅ | ❌ |
hapticFeedback built-in |
✅ | separate pkg | ❌ |
enableScrollByTapOnItem |
✅ | ✅ | ❌ |
| Web fallback | ✅ | ❌ | ❌ |
| TimeDrumPicker (12h/24h) | ✅ | ❌ | ❌ |
Quality and reliability
This isn't a weekend hack. The library has:
- 8 CI jobs on every PR: lint, typecheck, Jest (90+ tests), Android build, iOS build, Android instrumented tests (Espresso on real emulator), iOS XCTest (pod lib lint), and Detox E2E tests on both simulators
-
Branch protection — merging to
mainwithout all 8 checks passing is blocked -
Automated releases via release-please — merge a
feat:PR and a Release PR appears automatically; merge it and npm publishes automatically - Full TypeScript with generics —
DrumPicker<CountryCode>gives you typedonChangeevents
Installation
npm install react-native-drum-picker
# or
yarn add react-native-drum-picker
Requires React Native ≥ 0.73 with New Architecture enabled.
import DrumPicker, {
DateDrumPicker,
TimeDrumPicker,
withVirtualized,
usePickerGroup,
usePickerGroupChangedEffect,
usePickerGroupChangingEffect,
type DrumPickerRef,
type DateDrumPickerRef,
} from 'react-native-drum-picker';
Links
- 📦 npm: react-native-drum-picker
- ⭐ GitHub: scrollDynasty/react-native-drum-picker
If this saves you time on your next project, a ⭐ on GitHub
means a lot. Issues, PRs, and feedback are very welcome.
Built in Tashkent 🇺🇿 by Umar
Top comments (0)