Native Fabric Wheel Picker for React Native – drum-picker

Description:

react-native-drum-picker is a Fabric React Native component that creates an iOS-style wheel picker with native Kotlin and Swift implementations.

It includes controlled selection, date columns, custom rows, circular values, imperative refs, and virtualized large lists on Android and iOS.

Features

  • Native iOS-style wheel snapping on Android and iOS.
  • Optional center selection indicator and haptic feedback.
  • Controlled index, value callbacks, and imperative scrolling.
  • Generic item arrays with custom React row renderers.
  • Date columns through DateDrumPicker.
  • Circular scrolling for bounded repeating values.
  • Virtualized windows for large picker lists.
  • Picker groups for synchronized columns.

How To Use It

Install and rebuild the native app

# Add the Fabric wheel picker.
npm install react-native-drum-picker
# Install iOS pods, then rebuild the native app.
npx pod-install
npx react-native run-ios
# Run the Android rebuild when the Android target is ready.
npx react-native run-android

Expo apps need expo prebuild and a development build:

# Install the native package in an Expo project.
npx expo install react-native-drum-picker
npx expo prebuild
# Compile the generated native project.
npx expo run:ios
# npx expo run:android

Render a basic wheel

Pass an array through items, set the selected row, and read the native event from onChange:

import { useState } from "react";
import { Text, View } from "react-native";
import { DrumPicker } from "react-native-drum-picker";
const DAYS = ["Mon 8 Jun", "Tue 9 Jun", "Wed 10 Jun", "Thu 11 Jun"];
export function DeliveryDayField() {
  const [selectedIndex, setSelectedIndex] = useState(1);
  return (
    <View>
      <DrumPicker
        items={DAYS}
        selectedIndex={selectedIndex}
        itemHeight={44}
        visibleItemCount={5}
        onChange={(event) => {
          // Use the settled row for form state or persistence.
          setSelectedIndex(event.nativeEvent.index);
          console.log("Selected day:", event.nativeEvent.value);
        }}
        style={{ width: 150, height: 220 }}
      />
      <Text>{DAYS[selectedIndex]}</Text>
    </View>
  );
}

Handle live wheel movement

onChange fires after the wheel snaps to idle and the centered index changes. onValueChanging fires during the scroll and can run many times per second:

import { useState } from "react";
import { Text } from "react-native";
import { DrumPicker } from "react-native-drum-picker";
export function LiveTemperaturePicker() {
  const [preview, setPreview] = useState("21");
  return (
    <>
      <Text>Preview: {preview}°C</Text>
      <DrumPicker
        items={["18", "19", "20", "21", "22", "23", "24"]}
        selectedIndex={3}
        onValueChanging={({ nativeEvent }) => {
          // Keep this handler light because it runs during the drag.
          setPreview(nativeEvent.value);
        }}
        onChange={({ nativeEvent }) => {
          // Use the settled event for the committed value.
          setPreview(nativeEvent.value);
        }}
        style={{ width: 110, height: 220 }}
      />
    </>
  );
}

Render a constrained date picker

DateDrumPicker builds day, month, and year columns, adjusts day ranges for the selected month, and accepts inclusive minDate and maxDate constraints:

import { useState } from "react";
import { DateDrumPicker, type DateDrumPickerValue } from "react-native-drum-picker";
export function BookingDateField() {
  const [date, setDate] = useState<DateDrumPickerValue>({
    day: 12,
    month: 6,
    year: 2026,
  });
  return (
    <DateDrumPicker
      mode="day-month-year"
      value={date}
      onChange={setDate}
      monthFormat="long"
      locale="en-US"
      minDate={{ day: 1, month: 6, year: 2026 }}
      maxDate={{ day: 30, month: 9, year: 2026 }}
      itemHeight={44}
      visibleItemCount={5}
    />
  );
}

Scroll through a ref

Use DrumPickerRef for resets, jumps, and value lookup:

import { useRef } from "react";
import { Button } from "react-native";
import { DrumPicker, type DrumPickerRef } from "react-native-drum-picker";
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"];
export function MonthPickerActions() {
  const pickerRef = useRef<DrumPickerRef>(null);
  return (
    <>
      <DrumPicker
        ref={pickerRef}
        items={MONTHS}
        style={{ width: 112, height: 220 }}
      />
      <Button
        title="Jump to June"
        onPress={() => pickerRef.current?.scrollToValue("Jun")}
      />
      <Button
        title="Reset"
        onPress={() => pickerRef.current?.scrollToIndex(0, { animated: true })}
      />
    </>
  );
}

Render custom rows and large lists

Use renderItem for labels with flags, icons, or selected-row styling. Wrap the component with withVirtualized when the source array is large:

import { Text } from "react-native";
import { DrumPicker, withVirtualized } from "react-native-drum-picker";
const VirtualizedCityPicker = withVirtualized(DrumPicker);
const CITIES = Array.from({ length: 1500 }, (_, index) => `City ${index + 1}`);
export function CityField() {
  return (
    <VirtualizedCityPicker
      items={CITIES}
      selectedIndex={12}
      windowSize={20}
      renderItem={({ item, isSelected }) => (
        <Text style={{ color: isSelected ? "#111827" : "#9CA3AF" }}>
          {item}
        </Text>
      )}
      onChange={({ nativeEvent }) => console.log(nativeEvent.value)}
      style={{ width: 180, height: 220 }}
    />
  );
}

Available Component Props

DrumPicker

PropTypeDefaultDescription
itemsT[]requiredGeneric picker values. Strings and objects are supported.
selectedIndexnumber0Selected row index.
circularbooleanfalseWraps the last item to the first and the first item to the last.
itemHeightnumber44Row height in density-independent pixels.
visibleItemCountnumber5Number of visible rows. An odd value keeps the center symmetric.
textColorstring#8E8E93Unselected text color.
selectedTextColorstring#1C1C1ESelected text color.
textSizenumber20Unselected text size.
selectedTextSizenumber22Selected text size.
showSelectionIndicatorbooleantrueShows the center selection lines.
selectionIndicatorColorstring#D1D1D6Selection line color.
selectionIndicatorHeightnumber1Selection line thickness.
backgroundColorstringtransparentRoot view background.
containerBackgroundColorstringtransparentNative RecyclerView background.
itemBackgroundColorstringtransparentRow background.
hapticFeedbackbooleanfalseAdds light haptic feedback when the wheel snaps.
disabledbooleanfalseBlocks user drags and taps while programmatic scrolling remains active.
enableScrollByTapOnItembooleanfalseScrolls a visible row to the center when the user taps it.
onChange(event: NativeSyntheticEvent<DrumPickerChangeEvent>) => voidundefinedFires with nativeEvent.index and nativeEvent.value after the wheel settles.
onValueChanging(event: NativeSyntheticEvent<DrumPickerChangeEvent>) => voidundefinedFires on every scroll tick during a drag.
pickerGroupPickerGroupHandleundefinedConnects the picker to a group from usePickerGroup.
pickerNamestringundefinedUnique name used inside a picker group.
renderItem(info: DrumPickerRenderItemInfo<T>) => ReactNodeundefinedRenders a custom row.
styleStyleProp<ViewStyle>undefinedSets picker dimensions and layout.
testIDstringundefinedTest identifier for the native view.

DateDrumPicker

PropTypeDefaultDescription
modeDateDrumPickerModeday-month-yearColumn order and selection fields.
valueDateDrumPickerValueundefinedControlled { day, month, year } value.
onChange(value: DateDrumPickerValue) => voidundefinedFires with the current date value.
onValueChanging(column: DateDrumPickerColumnKey, event: NativeSyntheticEvent<DrumPickerChangeEvent>) => voidundefinedFires while a day, month, or year column scrolls.
minYearnumbercurrent year minus 100First year in the legacy year range.
maxYearnumbercurrent year plus 50Last year in the legacy year range.
minDateDateConstraintundefinedInclusive minimum date. Partial fields are supported.
maxDateDateConstraintundefinedInclusive maximum date. Partial fields are supported.
monthFormat"short" | "long" | "number"shortMonth label format.
localestringenIntl locale for month labels.
itemHeightnumber44Row height passed to each column.
visibleItemCountnumber5Visible rows passed to each column.
textColorstringundefinedUnselected text color for each column.
selectedTextColorstringundefinedSelected text color for each column.
textSizenumberundefinedUnselected text size for each column.
selectedTextSizenumberundefinedSelected text size for each column.
showSelectionIndicatorbooleanundefinedShows the center lines in each column.
selectionIndicatorColorstringundefinedCenter line color for each column.
selectionIndicatorHeightnumberundefinedCenter line thickness for each column.
backgroundColorstringtransparentBackground passed to each column.
itemBackgroundColorstringtransparentRow background passed to each column.
containerBackgroundColorstringtransparentNative container background for each column.
hapticFeedbackbooleanfalseHaptic feedback setting for each column.
disabledbooleanfalseBlocks user interaction on every column.
enableScrollByTapOnItembooleanfalseCenters a visible row after a tap.
styleStyleProp<ViewStyle>undefinedStyle for the row containing the columns.
columnStyleStyleProp<ViewStyle>undefinedShared style for every column.
columnStylesPartial<Record<"day" | "month" | "year", StyleProp<ViewStyle>>>undefinedPer-column styles.
columnTestIDsPartial<Record<"day" | "month" | "year", string>>undefinedPer-column test IDs.

Date modes

ModeColumns from left to right
dayday
monthmonth
yearyear
day-monthday, month
month-yearmonth, year
day-month-yearday, month, year
month-day-yearmonth, day, year
year-month-dayyear, month, day

withVirtualized props

PropTypeDefaultDescription
windowSizenumber20Rows rendered above and below the selected window.
windowRecenterDebounceMsnumber100Delay before the native slice recenters at a window edge.

Custom row data

renderItem receives this object:

FieldTypeDescription
itemTRaw value from items.
labelstringDisplay label resolved by the package.
indexnumberIndex in the source array.
isSelectedbooleanIndicates the centered row.

Events, Refs, and Hooks

Ref methods

// Scroll to a valid item index. The default is animated scrolling.
pickerRef.current?.scrollToIndex(3, { animated: true });
// Scroll to the first item that matches the value string.
pickerRef.current?.scrollToValue("Apr");
// Read the current native selection.
const index = pickerRef.current?.getCurrentIndex();
const value = pickerRef.current?.getCurrentValue();

Picker groups

Use usePickerGroup when separate wheels need shared change events:

import { View } from "react-native";
import {
  DrumPicker,
  usePickerGroup,
  usePickerGroupChangedEffect,
  usePickerGroupChangingEffect,
} from "react-native-drum-picker";
export function TimeColumns() {
  const group = usePickerGroup();
  usePickerGroupChangedEffect(group, ({ pickerName, value }) => {
    // Update the final time value after a column settles.
    console.log(`${pickerName}: ${value}`);
  });
  usePickerGroupChangingEffect(group, ({ pickerName, value }) => {
    // Keep previews light during the wheel movement.
    console.log(`Preview ${pickerName}: ${value}`);
  });
  return (
    <View style={{ flexDirection: "row" }}>
      <DrumPicker
        pickerGroup={group}
        pickerName="hour"
        items={["08", "09", "10", "11"]}
        onChange={() => {}}
      />
      <DrumPicker
        pickerGroup={group}
        pickerName="minute"
        items={["00", "15", "30", "45"]}
        onChange={() => {}}
      />
    </View>
  );
}

Tags:

Add Comment