DEV Community

scroll
scroll

Posted on

I built the most feature-complete React Native drum picker — native Fabric, 10+ features, zero compromises

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';
Enter fullscreen mode Exit fullscreen mode

Basic usage

const [index, setIndex] = useState(0);

<DrumPicker
  items={['January', 'February', 'March', ...]}
  selectedIndex={index}
  onChange={({ nativeEvent }) => setIndex(nativeEvent.index)}
/>
Enter fullscreen mode Exit fullscreen mode

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}
/>
Enter fullscreen mode Exit fullscreen mode

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)}
/>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 })}
/>
Enter fullscreen mode Exit fullscreen mode

10,000+ items with virtualization

const VirtualizedDrumPicker = withVirtualized(DrumPicker);

<VirtualizedDrumPicker
  items={allCitiesInTheWorld}  // 10,000+ items
  windowSize={20}
  selectedIndex={index}
  onChange={({ nativeEvent }) => setIndex(nativeEvent.index)}
/>
Enter fullscreen mode Exit fullscreen mode

Circular (infinite loop) scrolling

// 58 → 59 → 00 → 01
<DrumPicker
  circular
  items={minutes}
  onChange={({ nativeEvent }) => setMinute(nativeEvent.index)}
/>
Enter fullscreen mode Exit fullscreen mode

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);
  }}
/>
Enter fullscreen mode Exit fullscreen mode

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 main without 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 typed onChange events

Installation

npm install react-native-drum-picker
# or
yarn add react-native-drum-picker
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

Links

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)