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

@iiyu/react-native-wheel-picker

v1.0.1

Published

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

Downloads

594

Readme

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.

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.

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.

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

| Prop | Type | Default | Description | | -------------- | ------------------------------------------ | -------------------------------------- | -------------------------------------------------------------------- | | data | readonly T[] | required | Items rendered by the picker. | | renderItem | ({ item, index, selected }) => ReactNode | primitive/React-node renderer | Renders a row. selected follows the row nearest the visual center. | | keyExtractor | (item, index) => string | primitive value, element key, or index | Supplies stable, unique FlatList keys. | | renderEmpty | () => ReactNode | default empty message | Replaces the empty state. |

Selection and interaction

| Prop | Type | Default | Description | | ---------------------- | ------------------------------ | -------------- | ------------------------------------------------- | | selectedIndex | number | — | Controlled committed index. | | defaultSelectedIndex | number | 0 | Initial uncontrolled index. | | onValueChange | (item, index) => void | — | Fires once when a new selection settles. | | enabled | boolean | true | Enables drag, tap, and accessibility adjustment. | | animated | boolean | true | Default for programmatic selection transitions. | | loop | boolean | false | Repeats multi-item data for continuous scrolling. | | tapToSelect | boolean | false | Centers a visible row when it is pressed. | | decelerationRate | number \| 'normal' \| 'fast' | 'fast' | Controls native scroll deceleration. | | reducedMotion | boolean | system setting | Overrides the system reduce-motion preference. |

Geometry, slots, and style

| Prop | Type | Default | Description | | ------------------------ | ---------------------------- | ---------------- | -------------------------------------------------- | | itemHeight | number | 44 | Fixed height and snap interval for every row. | | visibleItemCount | number | 5 | Positive odd number of visible row extents. | | header / footer | ReactNode | — | Natural-height content outside the wheel viewport. | | renderSelectionOverlay | (info) => ReactNode | default band | Replaces the centered, non-interactive overlay. | | style | StyleProp<ViewStyle> | — | Root style. | | viewportStyle | StyleProp<ViewStyle> | — | Wheel viewport style. | | contentContainerStyle | StyleProp<ViewStyle> | — | FlatList content style. | | styles | Partial<WheelPickerStyles> | — | Named style slots described below. | | animation | WheelPickerAnimationConfig | platform preset | Overrides the wheel-depth treatment. | | testID | string | 'wheel-picker' | Test identifier applied to the root. |

Accessibility

| Prop | Type | Default | Description | | --------------------------- | ------------------------- | ---------------------- | ---------------------------------------------------------- | | accessibilityLabel | string | 'Wheel picker' | Labels the adjustable control. | | accessibilityHint | string | — | Adds screen-reader instructions. | | getItemAccessibilityLabel | (item, index) => string | primitive string value | Produces the committed value announced by a screen reader. |

Animation configuration

animation accepts:

| Field | Description | | ---------------- | ------------------------------------------------------------- | | perspective | Positive camera distance for the 3D transform. Default 900. | | maxRotation | Maximum absolute rotateX angle in degrees. | | minimumOpacity | Edge opacity from 0 to 1. | | minimumScale | Edge 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.

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.