Sign In

@iiyu/react-native-wheel-picker

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@iiyu/react-native-wheel-picker

A performant and customizable wheel picker for React Native and Expo.

latest
Source
npmnpm
Version
1.0.1
Version published
Weekly downloads
10
-33.33%
Maintainers
1
Weekly downloads
 
Created
Source

React Native Wheel Picker

A performant, customizable wheel picker built with React Native's core Animated API. It works in Expo and React Native without native modules or a config plugin. Use it as a single picker or compose multiple wheels into controls such as a time picker.

Three synchronized wheel pickers composing an accessible time picker

Try it in Expo Go

Open the interactive example in Expo Snack, select My Device, and scan Snack's live QR code with Expo Go. Alternatively, scan the permanent QR below on iOS or Android and tap Open with Expo Go.

Open the React Native Wheel Picker example in Expo Snack

Installation

Install the package from npm:

npm install @iiyu/react-native-wheel-picker

React >=19.1 and React Native >=0.81 are peer dependencies. Expo projects already provide both.

Quick start

import { useState } from 'react';
import { Text } from 'react-native';
import { WheelPicker } from '@iiyu/react-native-wheel-picker';

const sizes = ['Small', 'Medium', 'Large'] as const;

export function SizePicker() {
  const [selectedIndex, setSelectedIndex] = useState(1);

  return (
    <WheelPicker
      accessibilityLabel="Size"
      data={sizes}
      selectedIndex={selectedIndex}
      onValueChange={(_, index) => setSelectedIndex(index)}
      renderItem={({ item }) => <Text>{item}</Text>}
    />
  );
}

Width and height props are not required. The picker stretches to its parent and derives its viewport height from itemHeight (default 44) multiplied by visibleItemCount (default 5).

Selection behavior

Use selectedIndex with onValueChange for controlled state, or defaultSelectedIndex for uncontrolled state. Indexes are canonical so primitive values, objects, duplicate values, and React elements are all supported.

While a user drags or momentum is running, the row nearest the center immediately receives visual selected styles. The public selection and onValueChange update once scrolling settles. This matches the native iOS distinction between visual focus and the committed value and avoids callback updates on every scroll frame.

<WheelPicker
  data={['One', 'Two', 'Three']}
  defaultSelectedIndex={1}
  onValueChange={(item, index) => {
    console.log(item, index);
  }}
/>

For object data, provide a renderer and stable keys:

type Person = { id: string; name: string };

<WheelPicker<Person>
  data={people}
  keyExtractor={(person) => person.id}
  renderItem={({ item, selected }) => (
    <Text style={{ fontWeight: selected ? '700' : '400' }}>{item.name}</Text>
  )}
/>;

Strings, numbers, and valid React nodes have a default renderer. Plain objects require renderItem. A custom renderer can combine text, avatars, badges, and any other React content while the picker continues to handle centering, selection, and accessibility.

Custom teammate picker with avatars and status badges in a light theme Custom teammate picker with avatars and status badges in a dark theme

Optional looping

Set loop to let a picker with two or more items scroll continuously in both directions:

<WheelPicker
  data={['Low', 'Medium', 'High']}
  defaultSelectedIndex={1}
  loop
  onValueChange={(item, index) => {
    console.log(item, index);
  }}
/>

Repeated rows are an internal positioning detail. selectedIndex, onValueChange, renderItem, renderSelectionOverlay, getItemAccessibilityLabel, and the imperative ref always use indexes from the original data. Crossing from the last item to the first therefore commits index 0, and crossing upward from the first commits data.length - 1.

Empty and one-item pickers retain their normal finite, non-adjustable behavior when loop is enabled. Finite scrolling remains the default.

Fixed row height

Every row has exactly the configured itemHeight. This is an intentional invariant: deterministic snapping, center alignment, constant-time getItemLayout, and bounded FlatList virtualization all depend on a fixed row extent.

Custom content is centered and clipped inside that row. It may have any internal layout, but it must not be expected to change the row's measured height. Increase itemHeight if content or large text needs more vertical space. Variable-height rows are not supported.

visibleItemCount must be a positive odd integer so one complete row remains at the exact center. Invalid geometry warns in development and falls back to a safe default.

API

Data and rendering

PropTypeDefaultDescription
datareadonly T[]requiredItems rendered by the picker.
renderItem({ item, index, selected }) => ReactNodeprimitive/React-node rendererRenders a row. selected follows the row nearest the visual center.
keyExtractor(item, index) => stringprimitive value, element key, or indexSupplies stable, unique FlatList keys.
renderEmpty() => ReactNodedefault empty messageReplaces the empty state.

Selection and interaction

PropTypeDefaultDescription
selectedIndexnumberControlled committed index.
defaultSelectedIndexnumber0Initial uncontrolled index.
onValueChange(item, index) => voidFires once when a new selection settles.
enabledbooleantrueEnables drag, tap, and accessibility adjustment.
animatedbooleantrueDefault for programmatic selection transitions.
loopbooleanfalseRepeats multi-item data for continuous scrolling.
tapToSelectbooleanfalseCenters a visible row when it is pressed.
decelerationRatenumber | 'normal' | 'fast''fast'Controls native scroll deceleration.
reducedMotionbooleansystem settingOverrides the system reduce-motion preference.

Geometry, slots, and style

PropTypeDefaultDescription
itemHeightnumber44Fixed height and snap interval for every row.
visibleItemCountnumber5Positive odd number of visible row extents.
header / footerReactNodeNatural-height content outside the wheel viewport.
renderSelectionOverlay(info) => ReactNodedefault bandReplaces the centered, non-interactive overlay.
styleStyleProp<ViewStyle>Root style.
viewportStyleStyleProp<ViewStyle>Wheel viewport style.
contentContainerStyleStyleProp<ViewStyle>FlatList content style.
stylesPartial<WheelPickerStyles>Named style slots described below.
animationWheelPickerAnimationConfigplatform presetOverrides the wheel-depth treatment.
testIDstring'wheel-picker'Test identifier applied to the root.

Accessibility

PropTypeDefaultDescription
accessibilityLabelstring'Wheel picker'Labels the adjustable control.
accessibilityHintstringAdds screen-reader instructions.
getItemAccessibilityLabel(item, index) => stringprimitive string valueProduces the committed value announced by a screen reader.

Animation configuration

animation accepts:

FieldDescription
perspectivePositive camera distance for the 3D transform. Default 900.
maxRotationMaximum absolute rotateX angle in degrees.
minimumOpacityEdge opacity from 0 to 1.
minimumScaleEdge scale from 0 to 1.

Rows continue fading, scaling, rotating, and cylindrically compressing toward the viewport edges. Reduced motion suppresses those scroll-linked effects.

Imperative ref

import { createRef } from 'react';
import {
  WheelPicker,
  type WheelPickerRef,
} from '@iiyu/react-native-wheel-picker';

const pickerRef = createRef<WheelPickerRef>();

<WheelPicker ref={pickerRef} data={['One', 'Two']} />;

pickerRef.current?.scrollToIndex(1);
pickerRef.current?.scrollToIndex(0, { animated: false });
const selectedIndex = pickerRef.current?.getSelectedIndex();

Imperative indexes always refer to the original data and out-of-range values are clamped to that range. In loop mode, the picker animates to the nearest repeated occurrence, so programmatic changes across the first/last boundary take the short path. The package also exports DEFAULT_ITEM_HEIGHT and DEFAULT_VISIBLE_ITEM_COUNT.

Styling

The styles prop supports every visual layer:

  • container, viewport, and contentContainer
  • headerContainer and footerContainer
  • itemContainer and selectedItemContainer
  • text and selectedText
  • selectionOverlay
  • emptyContainer and emptyText
<WheelPicker
  data={['Small', 'Medium', 'Large']}
  header={<Text>Size</Text>}
  footer={<Text>Choose one option</Text>}
  tapToSelect
  animation={{
    perspective: 900,
    maxRotation: 62,
    minimumOpacity: 0.12,
    minimumScale: 0.78,
  }}
  styles={{
    selectionOverlay: {
      backgroundColor: '#eef2ff',
      borderColor: '#c7d2fe',
      borderWidth: 1,
    },
    selectedText: { color: '#312e81' },
  }}
/>

renderSelectionOverlay receives the visual selectedIndex, selectedItem, itemHeight, and viewportHeight. The overlay uses pointerEvents="none" so it never blocks dragging.

Accessibility and reduced motion

The viewport is one adjustable VoiceOver/TalkBack control. Increment and decrement actions use the same deduplicated selection path as touch and imperative scrolling. They stop at finite boundaries and wrap when loop is enabled. Off-center rows are hidden from the accessibility tree to prevent noisy announcements.

The picker listens to the system reduce-motion preference. When active, scroll-linked opacity and 3D transforms remain static and programmatic selection is immediate. Use reducedMotion only for an application-specific override.

For large text, choose an itemHeight that fits the rendered text at the largest supported font scale; row height does not grow automatically.

Performance

Opacity and transforms are connected to native scrolling with useNativeDriver: true. JavaScript state changes only at center-threshold crossings, while public selection changes only at settlement.

The picker uses fixed getItemLayout geometry, bounded rendering batches, and a virtualized FlatList window. removeClippedSubviews is deliberately disabled because it can hide transformed rows on Android. Keep data, renderItem, keyExtractor, style objects, and label callbacks stable so memoized rows can skip unrelated renders.

Loop mode uses an odd, middle-anchored repeated window. Ordinary boundary crossings keep the exact physical row that settled, avoiding a visible reset; the list normalizes to the equivalent middle row only when it reaches a distant edge guard. Small data sets receive enough repeated references to keep a fast fling away from an edge; large data sets use three copies. FlatList still renders only its bounded virtualized window.

The Expo 10k items example demonstrates large-data positioning and jumps to the first, middle, and final row.

Wheel picker positioned at item 5,000 in a virtualized list of 10,000 items

Troubleshooting

A controlled picker returns to the old row

Update selectedIndex in onValueChange. Controlled props remain authoritative after a drag settles.

The highlighted row changes before my application value

This is expected. Highlighting follows the row nearest the center during motion; the callback and controlled value change only after motion stops.

Object rows are blank or keys warn

Plain objects need renderItem. Supply a keyExtractor that returns a stable, unique key, especially when values may repeat.

Custom content is clipped or rows look uneven

All rows use the same itemHeight. Increase it to fit the tallest content and avoid vertical margins that imply a different external row height.

The center is between two rows

Use a positive odd visibleItemCount. Even and invalid values fall back to the default in development with a warning.

Dragging is disabled

Check that enabled is not false and that no parent gesture responder or absolute overlay is intercepting touches. Custom selection overlays provided through this component cannot intercept touches.

Looping does not move a one-item picker

This is intentional. Continuous scrolling only becomes effective when data contains at least two items; empty and one-item pickers remain non-adjustable.

Metro resolves the repository instead of the packed package

Do not validate publication through the example's file:.. dependency alone. Run npm run test:expo-fixture; it packs the artifact, installs it into an isolated Expo app, and generates production iOS and Android Metro bundles.

Example app

npm install
npm run example:start

Or launch a local platform:

npm run example:ios
npm run example:android

The showcase includes basics, a composed time picker, custom JSX, finite and looping modes in the customization playground, 10,000 items, light/dark themes, and accessibility and reduced-motion behavior.

Development and release validation

npm install
npm run check
npm run release:check

check runs formatting, linting, root/example type checking, tests, package builds, consumer declaration compilation, and export verification. release:check additionally inspects the npm tarball and installs the real packed artifact into a clean Expo fixture before producing minified iOS and Android Metro bundles.

Set KEEP_EXPO_FIXTURE=1 when debugging the generated fixture.

Keywords

react-native

FAQs

Package last updated on 30 Jul 2026

Related posts