npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-native-drum-picker

v0.2.4

Published

Cross-platform native drum/wheel picker for React Native (Fabric)

Readme

react-native-drum-picker

CI codecov npm version license platform - android platform - ios React Native

A smooth cross-platform native iOS-style drum/wheel picker for React Native (Fabric / New Architecture).

Preview

iOS preview coming soon — see the ios-build CI job for validation status.

Features

  • Android native implementation (Kotlin + RecyclerView)
  • iOS native implementation (Swift + UIPickerView)
  • iOS-style wheel / drum picker with smooth snapping
  • Center selection indicator (optional)
  • Transparent background by default
  • Custom text colors and sizes
  • TypeScript API
  • Flexible DateDrumPicker wrapper (day / month / year columns)
  • Fabric View / New Architecture

Installation

yarn add react-native-drum-picker
npm install react-native-drum-picker

This package includes native Android and iOS code. Rebuild your app after installing:

npx pod-install
npx react-native run-ios
npx react-native run-android

Platform support

| Platform | Status | | -------- | ------------------------------------------------------------------------- | | Android | Supported | | iOS | Supported | | Web | Read-only preview stub (DrumPicker.tsx); ref API works, no native wheel |

Requires React Native 0.76+ with the New Architecture enabled.

Compatibility

| Environment | Status | | ---------------------------------- | ----------------------------------------- | | React Native New Architecture | Required | | Fabric | Required | | Android | Supported | | iOS | Supported | | iOS Old Architecture (Paper) | Not supported — Fabric required | | Expo Go | Not supported (native library) | | Expo SDK 54 + dev build / prebuild | Tested | | react-native-screens navigation | Tested (use 0.1.4+ for detach safety) |

Tested with

| Tool | Version | | ---------------- | ---------------------------- | | Expo SDK | 54 | | React Native | 0.81.5, 0.85.0 (example app) | | New Architecture | enabled | | Android | emulator / device |

Intended range: react-native >= 0.76 with New Architecture. The package is actively tested on RN 0.81.x / 0.85.x. Older 0.76–0.80 may work but are not CI-guaranteed.

This package is an Android Fabric View library. Use a development build or expo run:android after expo prebuild — not Expo Go.

React Native 0.81+ event dispatch

If Android Kotlin compile fails with No value passed for parameter 'uiManagerType', upgrade to 0.1.3+ (Fabric UIManagerType.FABRIC).

Basic usage

import { DrumPicker } from 'react-native-drum-picker';

export function Example() {
  return (
    <DrumPicker
      items={['Mon 7 Sep', 'Tue 8 Sep', 'Wed 9 Sep']}
      selectedIndex={1}
      itemHeight={44}
      visibleItemCount={5}
      onChange={(event) => {
        console.log(event.nativeEvent.index, event.nativeEvent.value);
      }}
      style={{ width: 150, height: 220 }}
    />
  );
}

Layout defaults (0.1.5+)

The JS wrapper applies minWidth: 64 and, unless you use flex or pass height / minHeight, height: itemHeight * visibleItemCount (default 220). Native Android also sets matching minimumWidth / minimumHeight.

For production layouts, still pass explicit dimensions:

style={{ width: 120, height: itemHeight * visibleItemCount }}

In __DEV__, a one-time warning is logged if neither height nor flex sizing is provided.

Examples

Basic picker

Basic picker demo

Time picker (hour · minute)

Time picker demo

const hours = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0'));
const minutes = Array.from({ length: 60 }, (_, i) =>
  String(i).padStart(2, '0')
);

<View style={{ flexDirection: 'row', alignItems: 'center' }}>
  <DrumPicker items={hours} style={{ width: 72, height: 220 }} />
  <DrumPicker items={minutes} style={{ width: 72, height: 220 }} />
</View>;

Height / weight row (onboarding style)

Height/weight demo

<View style={{ flexDirection: 'row', alignItems: 'center' }}>
  <DrumPicker items={heightsCm} style={{ width: 90, height: 220 }} />
  <DrumPicker items={weightsKg} style={{ width: 96, height: 220 }} />
</View>

Controlled selectedIndex

Controlled picker demo

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

<DrumPicker
  items={items}
  selectedIndex={index}
  onChange={(e) => setIndex(e.nativeEvent.index)}
  style={{ width: 120, height: 220 }}
/>;

onChange and expensive side effects

Debounce demo

Native emits onChange when the wheel snaps to idle and the centered index changes (duplicate indices are ignored). Use it for UI state. For AsyncStorage, APIs, or analytics, debounce in your app:

const save = useMemo(() => {
  let t: ReturnType<typeof setTimeout> | undefined;
  return (value: string) => {
    if (t) clearTimeout(t);
    t = setTimeout(() => {
      // persist value
    }, 300);
  };
}, []);

<DrumPicker onChange={(e) => save(e.nativeEvent.value)} ... />

Live sync between pickers

Use onValueChanging to update a preview while the user is still scrolling — before they lift their finger:

const [previewIndex, setPreviewIndex] = useState(1);

<DrumPicker
  items={['AM', 'PM']}
  onValueChanging={({ nativeEvent }) => {
    setPreviewIndex(nativeEvent.index);
  }}
  onChange={({ nativeEvent }) => {
    setPreviewIndex(nativeEvent.index);
  }}
/>;

onValueChanging can fire many times per second while the wheel moves. Keep the handler light; for UI updates prefer a ref or debounce/requestAnimationFrame instead of heavy setState on every tick. Use onChange for the final committed value.

See the example app (example/src/App.tsx) for basic, time, height/weight, date, controlled, and debounced demos.

Imperative ref API

Control the picker programmatically using a ref. scrollToIndex / scrollToValue default to animated: true and invoke onChange when the selection changes (so controlled selectedIndex stays in sync). withVirtualized forwards the same DrumPickerRef using real list indices.

import { useRef } from 'react';
import { Button } from 'react-native';
import { DrumPicker, type DrumPickerRef } from 'react-native-drum-picker';

const months = ['Jan', 'Feb', 'Mar', 'Jun'];

function MyPicker() {
  const ref = useRef<DrumPickerRef>(null);

  return (
    <>
      <DrumPicker
        ref={ref}
        items={months}
        onChange={({ nativeEvent }) => console.log(nativeEvent.value)}
      />
      <Button
        title="Jump to June"
        onPress={() => ref.current?.scrollToValue('Jun')}
      />
      <Button
        title="Reset to first"
        onPress={() => ref.current?.scrollToIndex(0, { animated: true })}
      />
    </>
  );
}

DateDrumPicker ref — "Today" button

Date picker demo

import { useRef, useState } from 'react';
import { Button } from 'react-native';
import {
  DateDrumPicker,
  type DateDrumPickerRef,
  type DateDrumPickerValue,
} from 'react-native-drum-picker';

function DateWithToday() {
  const [date, setDate] = useState<DateDrumPickerValue>({
    day: 1,
    month: 1,
    year: 2026,
  });
  const today = new Date();
  const dateRef = useRef<DateDrumPickerRef>(null);

  return (
    <>
      <DateDrumPicker
        ref={dateRef}
        mode="day-month-year"
        value={date}
        onChange={setDate}
      />
      <Button
        title="Today"
        onPress={() =>
          dateRef.current?.scrollToDate(
            {
              day: today.getDate(),
              month: today.getMonth() + 1,
              year: today.getFullYear(),
            },
            { animated: true }
          )
        }
      />
    </>
  );
}

DrumPickerRef API

| Method | Description | | -------------------------------- | --------------------------------------------------- | | scrollToIndex(index, options?) | Scroll to index. Clamped to valid range. | | scrollToValue(value, options?) | Scroll to first matching value. No-op if not found. | | getCurrentIndex() | Returns current selected index. | | getCurrentValue() | Returns current selected value string. |

DateDrumPickerRef API

| Method | Description | | ------------------------------ | ----------------------------------------------------------------------------------------------------------- | | scrollToDate(date, options?) | Scroll columns to given date. Partial updates supported. Clamps invalid days and calls onChange when set. | | getCurrentDate() | Returns clamped { day, month, year } of current selection. |

PickerGroup - synchronize multiple pickers

Connect multiple DrumPickers so they can react to each other:

import { useState } from 'react';
import { View } from 'react-native';
import {
  DrumPicker,
  usePickerGroup,
  usePickerGroupChangedEffect,
  usePickerGroupChangingEffect,
} from 'react-native-drum-picker';

const HOURS = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0'));
const MINUTES = [
  '00',
  '05',
  '10',
  '15',
  '20',
  '25',
  '30',
  '35',
  '40',
  '45',
  '50',
  '55',
];

function TimePickerGroup() {
  const group = usePickerGroup();
  const [time, setTime] = useState({ hour: 0, minute: 0 });

  usePickerGroupChangedEffect(group, ({ pickerName, index }) => {
    setTime((prev) => ({ ...prev, [pickerName]: index }));
  });

  usePickerGroupChangingEffect(group, ({ pickerName, value }) => {
    console.log(`${pickerName} is at ${value}`);
  });

  return (
    <View style={{ flexDirection: 'row' }}>
      <DrumPicker
        pickerGroup={group}
        pickerName="hour"
        items={HOURS}
        onChange={() => {}}
      />
      <DrumPicker
        pickerGroup={group}
        pickerName="minute"
        items={MINUTES}
        onChange={() => {}}
      />
    </View>
  );
}

PickerGroup API

| Export | Description | | ----------------------------------------- | ------------------------------------------------ | | usePickerGroup() | Creates a group handle. Call once per component. | | usePickerGroupChangedEffect(group, cb) | Fires when any picker settles. | | usePickerGroupChangingEffect(group, cb) | Fires on every scroll tick. | | group.getState() | Snapshot of all current picker values. |

DrumPicker group props

| Prop | Type | Description | | ------------- | ------------------- | ----------------------------- | | pickerGroup | PickerGroupHandle | Group from usePickerGroup() | | pickerName | string | Unique name within the group |

Custom item rendering

Use renderItem to replace the default text label with your own React UI:

import { DrumPicker } from 'react-native-drum-picker';
import { Text, View } from 'react-native';

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={{
          fontSize: 18,
          color: isSelected ? '#000' : '#999',
          fontWeight: isSelected ? '600' : '400',
        }}
      >
        {item.label}
      </Text>
    </View>
  )}
  onChange={({ nativeEvent }) => console.log(nativeEvent.value)}
/>;

renderItem info object

| Field | Type | Description | | ------------ | --------- | --------------------------- | | item | T | Raw item from items | | label | string | Display label string | | index | number | Index in items | | isSelected | boolean | Whether the row is centered |

Performance note: keep renderItem lightweight. It can rerender frequently during wheel movement. For complex rows, wrap heavy subtrees with React.memo and pass a stable renderer via useCallback.

const Row = React.memo(function Row({
  flag,
  label,
  isSelected,
}: {
  flag: string;
  label: string;
  isSelected: boolean;
}) {
  return (
    <View style={{ flexDirection: 'row', gap: 8 }}>
      <Text>{flag}</Text>
      <Text style={{ color: isSelected ? '#111' : '#999' }}>{label}</Text>
    </View>
  );
});

const renderCountry = useCallback(
  ({ item, isSelected }) => (
    <Row flag={item.flag} label={item.label} isSelected={isSelected} />
  ),
  []
);

DateDrumPicker

Higher-level date columns (TypeScript only). Renders wheels only — no built-in titles; add labels in your app if needed.

import { useState } from 'react';
import { DateDrumPicker } from 'react-native-drum-picker';

export function DateExample() {
  const [date, setDate] = useState({ day: 21, month: 5, year: 2026 });

  return (
    <DateDrumPicker mode="day-month-year" value={date} onChange={setDate} />
  );
}
<DateDrumPicker
  mode="month-year"
  monthFormat="long"
  minYear={2020}
  maxYear={2035}
  value={{ month: 5, year: 2026 }}
  onChange={(value) => console.log(value)}
/>

Controlled: pass value and update in onChange.
Uncontrolled: omit value; internal state updates and onChange still fires.

Day count follows month/year (e.g. February has 28/29 days).

Date range constraints

Booking constraints demo

import { useState } from 'react';
import {
  DateDrumPicker,
  type DateDrumPickerValue,
} from 'react-native-drum-picker';

function BookingDatePicker() {
  const [bookingDate, setBookingDate] = useState<DateDrumPickerValue>({});
  const today = new Date();
  const nextYear = new Date();
  nextYear.setFullYear(today.getFullYear() + 1);

  return (
    <DateDrumPicker
      mode="day-month-year"
      minDate={{
        day: today.getDate(),
        month: today.getMonth() + 1,
        year: today.getFullYear(),
      }}
      maxDate={{
        day: nextYear.getDate(),
        month: nextYear.getMonth() + 1,
        year: nextYear.getFullYear(),
      }}
      value={bookingDate}
      onChange={setBookingDate}
    />
  );
}

minDate / maxDate take precedence over minYear / maxYear when both are set.

onValueChanging, if used, receives the column key first: (column, event) => … where column is 'day' | 'month' | 'year'. Event nativeEvent.index uses calendar indices (month 1–12 → index 0–11; day uses day-of-month minus 1).

withVirtualized

Large list virtualized demo

For large item lists (cities, timezones, country codes), wrap DrumPicker with withVirtualized to render only items near the visible window:

import { DrumPicker, withVirtualized } from 'react-native-drum-picker';

const VirtualizedDrumPicker = withVirtualized(DrumPicker);

const CITIES = ['Tashkent', 'Moscow', 'London' /* ... */]; // 1000+ items

<VirtualizedDrumPicker
  items={CITIES}
  selectedIndex={selectedIndex}
  windowSize={20} // items above + below visible area (default: 20)
  onChange={({ nativeEvent }) => setIndex(nativeEvent.index)}
/>;

windowSize controls the render buffer. Higher = smoother fast flings, higher memory. Default of 20 works for most cases.

Optional windowRecenterDebounceMs (default 100) debounces slice recentering when you reach the first or last row of the current window — this prevents scroll feedback loops during fast flings on large lists.

Platforms: iOS and Android only (wrap DrumPicker from the package — not web). The native wheel always receives a small sliced items array on both platforms.

Requirements: each entry in items must be a unique string. Duplicate labels break index recovery during slice swaps and on iOS tap hit-testing.

Not intended for DateDrumPicker (small fixed column lists).

onValueChanging is supported: indices are remapped to the full list (same as onChange), so live preview works on large lists.

Circular scroll

Enable infinite looping scroll - when user scrolls past the last item it wraps to the first:

// Minutes: 58 -> 59 -> 00 -> 01
<DrumPicker
  circular
  items={minutes}
  onChange={({ nativeEvent }) => setMinute(nativeEvent.index)}
/>

// Combine with TimeDrumPicker
<TimeDrumPicker circular />

Best for: hours, minutes, seconds, days of week, months. Not recommended for: years, long lists (cities, countries).

Note: circular and withVirtualized can be used together but for lists > 100 items prefer one or the other.

withVirtualized(DrumPicker) props

In addition to all DrumPicker props (on the wrapped instance):

| Prop | Type | Default | Description | | -------------------------- | -------- | ------- | ---------------------------------------------------------------------- | | windowSize | number | 20 | Rows rendered above and below the selection | | windowRecenterDebounceMs | number | 100 | Debounce before shifting the slice when scrolling hits the window edge |

API reference

DrumPicker

| Prop | Type | Default | Description | | -------------------------- | --------------------- | ------------- | ------------------------------------------------------------------------------------ | | items | T[] | required | Picker items typed by generic T (strings or objects). Display/value mapping follows built-in label resolution and/or renderItem. | | selectedIndex | number | 0 | Selected row index | | itemHeight | number | 44 | Row height (dp) | | visibleItemCount | number | 5 | Visible rows (odd recommended) | | textColor | string | #8E8E93 | Unselected text | | selectedTextColor | string | #1C1C1E | Selected text | | textSize | number | 20 | Unselected size (sp) | | selectedTextSize | number | 22 | Selected size (sp) | | backgroundColor | string | transparent | Root view background | | containerBackgroundColor | string | transparent | RecyclerView background | | itemBackgroundColor | string | transparent | Row background | | showSelectionIndicator | boolean | true | Center lines | | selectionIndicatorColor | string | #D1D1D6 | Line color | | selectionIndicatorHeight | number | 1 | Line thickness (dp) | | hapticFeedback | boolean | false | Light haptic on snap (Android + iOS) | | circular | boolean | false | Enable infinite loop scroll. Wraps last->first and first->last. | | enableScrollByTapOnItem | boolean | false | Tap a visible row to scroll it to center (Android + iOS) | | onChange | function | — | nativeEvent: { index, value } | | onValueChanging | function | — | Fires on each scroll tick while dragging. Use for live sync; debounce heavy UI work. | | renderItem | (info) => ReactNode | — | Custom row renderer. Receives { item, label, index, isSelected }. | | style | ViewStyle | — | Size and layout |

DateDrumPicker

| Prop | Type | Default | Description | | -------------------------- | ------------------------------- | ---------------- | ---------------------------------------------------------------------------- | | mode | DateDrumPickerMode | day-month-year | Which columns to show | | value | { day?, month?, year? } | — | Controlled value | | onChange | function | — | { day, month, year } | | onValueChanging | function | — | (column, event) => … while scrolling; column is day / month / year | | minYear | number | now − 100 | Year range start (use minDate for full date bounds) | | maxYear | number | now + 50 | Year range end (use maxDate for full date bounds) | | minDate | DateConstraint | — | Minimum selectable date (inclusive). Partial — omit any field. | | maxDate | DateConstraint | — | Maximum selectable date (inclusive). Partial — omit any field. | | monthFormat | 'short' \| 'long' \| 'number' | short | Month labels | | locale | string | en | Intl locale for month names | | itemHeight | number | 44 | Passed to each column | | visibleItemCount | number | 5 | Passed to each column | | textColor | string | — | Passed to each column | | selectedTextColor | string | — | Passed to each column | | textSize | number | — | Passed to each column | | selectedTextSize | number | — | Passed to each column | | showSelectionIndicator | boolean | — | Passed to each column | | selectionIndicatorColor | string | — | Passed to each column | | selectionIndicatorHeight | number | — | Passed to each column | | backgroundColor | string | transparent | Passed to each column | | itemBackgroundColor | string | transparent | Passed to each column | | containerBackgroundColor | string | transparent | Passed to each column | | hapticFeedback | boolean | false | Passed to each column | | enableScrollByTapOnItem | boolean | false | Passed to each column | | style | ViewStyle | — | Row container | | columnStyle | ViewStyle | — | All columns | | columnStyles | object | — | Per column: day, month, year |

DateDrumPicker modes

Column order is left → right:

| mode | Columns | | ---------------- | ---------------- | | day | day | | month | month | | year | year | | day-month | day, month | | month-year | month, year | | day-month-year | day, month, year | | month-day-year | month, day, year | | year-month-day | year, month, day |

type DateDrumPickerMode =
  | 'day'
  | 'month'
  | 'year'
  | 'day-month'
  | 'month-year'
  | 'day-month-year'
  | 'month-day-year'
  | 'year-month-day';

Styling

Backgrounds are transparent by default. Only text and optional indicator lines are visible.

<DrumPicker
  items={['Small', 'Medium', 'Large']}
  selectedTextColor="#111827"
  textColor="#9CA3AF"
  selectionIndicatorColor="#D1D1D6"
  backgroundColor="transparent"
  style={{ width: 120, height: 220 }}
/>

Use an odd visibleItemCount (e.g. 5) for a symmetric wheel.

Troubleshooting

| Issue | What to try | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | Must rebuild after install | Native library — run a full Android rebuild | | Metro shows old code | npx react-native start --reset-cache | | Gradle / build errors | cd android && ./gradlew clean (Windows: .\gradlew clean) | | adb not found | Install Android SDK Platform-Tools; add to PATH | | Empty or white picker | Enable New Architecture; upgrade to 0.1.5+ for layout defaults; or set style={{ width, height: itemHeight * visibleItemCount }} | | Wrong initial row / off-center | Upgrade to 0.1.5+; avoid key remount hacks unless needed for other reasons | | Props not applied | Rebuild app after native changes; run yarn build in the library before packing | | iOS build issues | Run pod install in example/ios; use New Architecture; see CONTRIBUTING.md | | Expo Go | Use prebuild + dev build; this library is not in Expo Go | | RN 0.81 uiManagerType compile error | Upgrade to 0.1.3+ | | Crash when leaving a screen | Upgrade to 0.1.4+ (safe onDetachedFromWindow with react-native-screens) | | npm shows “no README” | Often a registry UI lag; run npm view react-native-drum-picker readme — if content appears, hard-refresh the package page |

Android build fails in Expo / React Native 0.81+

cd android
./gradlew clean

Windows:

cd android
.\gradlew clean

Then rebuild the native app (npx expo run:android or npx react-native run-android).

Crash when leaving a screen

If the app crashes when navigating away from a screen with DrumPicker (especially with react-native-screens transitions), upgrade to 0.1.4+, which avoids unsafe RecyclerView cleanup during onDetachedFromWindow.

New Architecture: This library is a Fabric view. Ensure New Architecture is enabled in your app (required for RN 0.76+).

Development & contributing

git clone https://github.com/scrollDynasty/react-native-drum-picker.git
cd react-native-drum-picker
yarn
yarn build
cd example
yarn android   # or yarn ios

Before a pull request:

yarn lint
yarn build
yarn typecheck
yarn test

Contributing: CONTRIBUTING.md — setup, where to put code and tests, what CI runs on every PR, and review checklist.
Example app: example/README.md.

License

MIT © Umar Matyokubov