WheelPickerNew

A vertical wheel picker with snap-to-row selection, distance-based fade and scale, and optional fade overlays.

Import

import { WheelPicker } from 'heroui-native-pro';

Anatomy

<WheelPicker items={[...]}>
  <WheelPicker.Indicator />
  <WheelPicker.Mask />
</WheelPicker>
  • WheelPicker: Root container. Manages controllable selection through value / defaultValue / onValueChange, owns the shared scroll offset, and renders a data-driven scrolling list of rows. Cascades animation settings to children and auto-renders a default indicator when no compound children are passed.
  • WheelPicker.Item: Animated row container. Auto-rendered for every option, or used as the outer element inside a custom renderItem. When no children are provided, renders WheelPicker.ItemLabel with the option's label. Tapping a row scrolls the wheel to focus it.
  • WheelPicker.ItemLabel: Default label primitive. Used by the auto-fallback inside WheelPicker.Item; reuse inside a custom renderItem to keep the default label styling.
  • WheelPicker.Indicator: Optional selection band rendered absolutely at the center of the viewport. Purely visual — selection logic lives on the root.
  • WheelPicker.Mask: Optional top / bottom fade overlays that soften the wheel into the surrounding background.

Usage

Basic usage

Pass a list of { value, label } items and bind value / onValueChange. The root renders a default indicator when no compound children are present.

const [year, setYear] = useState(1995);

<WheelPicker value={year} onValueChange={setYear} items={YEAR_ITEMS} />;

With mask

Add a WheelPicker.Mask for top / bottom fade overlays. Explicit children replace the auto-rendered indicator, so include WheelPicker.Indicator too.

<WheelPicker value={year} onValueChange={setYear} items={YEAR_ITEMS}>
  <WheelPicker.Indicator />
  <WheelPicker.Mask />
</WheelPicker>

Uncontrolled

Use defaultValue to seed the initial selection without managing external state.

<WheelPicker defaultValue={1995} items={YEAR_ITEMS} onValueChange={...} />

Item height and visible count

Customize row height and the number of visible rows. visibleCount must be odd so a single row sits centered on the indicator.

<WheelPicker
  items={SLOT_ITEMS}
  defaultValue="13:30"
  itemHeight={56}
  visibleCount={5}
/>

Custom item render

Pass renderItem to compose custom content per row. Use WheelPicker.Item as the outer wrapper to preserve sizing and tap-to-focus behavior.

<WheelPicker
  value={reminder}
  onValueChange={setReminder}
  items={REMINDER_TYPE_ITEMS}
  renderItem={({ item, isSelected }) => (
    <WheelPicker.Item className="flex-row items-center justify-center gap-3">
      <ReminderIcon
        value={item.value}
        colorClassName={isSelected ? 'accent-accent' : 'accent-muted'}
      />
      <WheelPicker.ItemLabel className="font-medium">
        {item.label}
      </WheelPicker.ItemLabel>
    </WheelPicker.Item>
  )}
>
  <WheelPicker.Indicator />
  <WheelPicker.Mask />
</WheelPicker>

Custom indicator

Style the selection band via classNames on WheelPicker.Indicator, or pass any children for decorative content rendered inside the highlight slot (patterns, gradients, icons). Pair with overflow-hidden on the highlight when the children should be clipped to the rounded corners.

<WheelPicker items={DURATION_ITEMS} defaultValue={25}>
  <WheelPicker.Indicator
    classNames={{
      wrapper: 'px-2',
      highlight: 'bg-accent-soft border border-accent/15 overflow-hidden',
    }}
  >
    <MyIndicatorPattern />
  </WheelPicker.Indicator>
  <WheelPicker.Mask />
</WheelPicker>

Custom mask color

Override the fade color when the wheel sits on a non-background surface. Combine with height (number or percentage) to control how far the fade extends.

const overlayColor = useThemeColor('overlay');

<WheelPicker items={TIME_ITEMS} defaultValue="09:00">
  <WheelPicker.Indicator />
  <WheelPicker.Mask color={overlayColor} height="75%" />
</WheelPicker>;

Custom animation

Tune the per-item [edge, center] opacity, scale, and label color ranges. The label color animation is always active (defaulting to theme [foreground, accent-soft-foreground]) — text-* color classes on classNames.itemLabel are overridden by the animated value.

<WheelPicker
  items={REMINDER_TYPE_ITEMS}
  defaultValue="notification"
  animation={{
    opacity: { value: [0.4, 1] },
    scale: { value: [0.75, 1] },
    labelColor: { value: ['#888', '#000'] },
  }}
/>

Disabled

Block interaction and dim the wheel with isDisabled.

<WheelPicker items={YEAR_ITEMS} defaultValue={1995} isDisabled />

Programmatic scroll

Use the ref to drive the selection imperatively. scrollToValue finds the matching row via Object.is equality and is a no-op when the value is not in items.

const ref = useRef<WheelPickerRootRef>(null);

<WheelPicker ref={ref} items={YEAR_ITEMS} defaultValue={1995} />;

ref.current?.scrollToValue(2000);
ref.current?.scrollToIndex({ index: 0, animated: false });

Example

import { WheelPicker } from 'heroui-native-pro';
import { useState } from 'react';
import { Text, View } from 'react-native';

const CURRENT_YEAR = new Date().getFullYear();
const YEAR_ITEMS = Array.from({ length: 80 }, (_, index) => {
  const year = CURRENT_YEAR - 18 - index;
  return { value: year, label: String(year) };
});

export default function BirthYearPicker() {
  const [year, setYear] = useState(1995);

  return (
    <View className="flex-1 items-center justify-center px-5">
      <View className="items-center gap-6">
        <View className="items-center gap-1">
          <Text className="text-xs text-muted uppercase tracking-wider">
            Date of birth
          </Text>
          <Text className="text-2xl font-semibold text-foreground tabular-nums">
            {year}
          </Text>
        </View>
        <View className="w-[200px]">
          <WheelPicker
            value={year}
            onValueChange={setYear}
            items={YEAR_ITEMS}
            classNames={{ itemLabel: 'tabular-nums' }}
          >
            <WheelPicker.Indicator />
            <WheelPicker.Mask />
          </WheelPicker>
        </View>
      </View>
    </View>
  );
}

API Reference

WheelPicker

proptypedefaultdescription
childrenReact.ReactNode-Compound parts. When omitted, a default WheelPicker.Indicator is rendered (skipped inside a WheelPickerGroup)
itemsReadonlyArray<WheelPickerOption<T>>-List of { value, label } rows rendered by the wheel
itemHeightnumber44Pixel height of a single row. Drives snapping, layout, and animation math. Ignored when nested in a WheelPickerGroup (inherited from the group)
visibleCountnumber5Number of rows visible inside the viewport. Must be odd so one row sits centered on the indicator. Ignored when nested in a WheelPickerGroup (inherited from the group)
valueT-Controlled selected value. The row whose item.value matches becomes the selected row
defaultValueT-Initial value used when the wheel is uncontrolled
namestring-Identifies this wheel inside a WheelPickerGroup. When set and a group context exists, the wheel reads / writes its value via the group
isDisabledbooleanfalseDisables interaction. The wheel still renders the current selection
classNamestring-Additional CSS classes for the root container
classNamesElementSlots<WheelPickerRootSlots>-Additional CSS classes for individual root slots
stylesWheelPickerRootStyles-Inline styles for individual root slots
renderItemWheelPickerRenderItem<T>-Custom row renderer. When omitted, the default renderer shows item.label inside a WheelPicker.ItemLabel
keyExtractor(item: WheelPickerOption<T>, index: number) => stringPrimitive-aware defaultKey extractor for the underlying FlatList. Defaults to `${value}:${index}` for primitives and String(index) otherwise
onValueChange(value: T) => void-Fires when the selected row changes during scroll, on tap-to-focus, and on imperative scrollToIndex / scrollToValue
animationWheelPickerRootAnimation-Animation configuration for the per-item opacity / scale interpolation
refWheelPickerRootRef-Imperative ref exposing scrollToIndex and scrollToValue in addition to the underlying view
...ViewPropsOmit<ViewProps, 'children'>-All standard React Native View props are supported

WheelPickerOption

Single picker entry rendered as a row in the wheel.

proptypedescription
valueTUnique value used for selection comparison and scrollToValue lookup
labelstringDisplay label rendered by the default item renderer

WheelPickerRenderItemInfo

Argument passed to renderItem.

proptypedescription
itemWheelPickerOption<T>The option being rendered
indexnumberZero-based index of the option in items
isSelectedbooleanWhether the option sits on the center selection band
scrollYSharedValue<number>Shared scroll offset (UI thread) used to drive per-item animations
itemHeightnumberResolved row height used by layout and animation math
absDistanceSharedValue<number>Per-row absolute distance from center, in row units (0 = centered, 0.5 = selection boundary, 1+ = one full row or more away). Drive your own interpolate / interpolateColor on it for custom row animations

ElementSlots<WheelPickerRootSlots>

slotdescription
containerOuter viewport wrapping the scroll list and overlays
contentContainerScroll content container carrier; receives the vertical centering padding
itemPer-row animated container (see animated property notes below)
itemLabelDefault label text inside a row

The item slot animates opacity and transform (scale) for distance-based fade and scale. These properties cannot be overridden via className; use the animation prop to customize, or animation="disabled" to remove them entirely.

styles

slottypedescription
containerViewStyleInline style for the outer viewport
contentContainerViewStyleInline style for the scroll content container
itemViewStyleInline style for the per-row container
itemLabelTextStyleInline style for the default label text

WheelPickerRootAnimation

Animation configuration for the per-item opacity, scale, and (optionally) label color interpolation. Can be:

  • false or "disabled": Disable per-item fade, scale, and label color (rows snap without interpolation)
  • "disable-all": Disable the wheel's animations and cascade disable-all to animated descendants
  • true or undefined: Use default animations
  • object: Custom animation configuration
proptypedefaultdescription
opacity.value[number, number][0.5, 1][edge, center] opacity values. value[0] is opacity at the farthest visible offset
scale.value[number, number][0.85, 1][edge, center] scale values. value[0] is scale at the farthest visible offset
labelColor.value[string, string]Theme [foreground, accent-soft-foreground][edge, center] color values for the row label, interpolated via interpolateColor strictly inside the half-row selection band — non-selected rows resolve to exactly value[0] (edge)

WheelPickerImperativeMethods

Exposed via the root ref on top of the underlying View ref.

methodsignaturedescription
scrollToIndex(params: { index: number; animated?: boolean }) => voidScroll the wheel so the given index becomes the selected row
scrollToValue(value: unknown, options?: { animated?: boolean }) => voidScroll the wheel so the row matching value becomes the selected row. No-op when the value is not in items

WheelPicker.Indicator

proptypedefaultdescription
childrenReact.ReactNode-Optional content rendered inside the indicator's highlight slot (patterns, gradients, icons). Pair with overflow-hidden on the highlight so the content is clipped to the rounded corners
classNamestring-Additional CSS classes for the indicator container
classNamesElementSlots<WheelPickerIndicatorSlots>-Additional CSS classes for individual indicator slots
stylesPartial<Record<WheelPickerIndicatorSlots, ViewStyle>>-Inline styles for individual indicator slots
...ViewPropsViewProps-All standard React Native View props are supported

ElementSlots<WheelPickerIndicatorSlots>

slotdescription
wrapperAbsolutely-positioned band centered on the wheel viewport
highlightFilled rectangle rendered inside the wrapper

styles

slottypedescription
wrapperViewStyleInline style for the indicator wrapper
highlightViewStyleInline style for the indicator highlight

WheelPicker.Mask

proptypedefaultdescription
colorstringuseThemeColor('background')Solid color the gradient fades from. Accepts any RN color string. Falls back to the theme background color when omitted
heightnumber | string"100%"Height of each mask half. number = raw pixels; percentage scales the default fade height (((visibleCount - 1) / 4) * itemHeight)
classNamestring-Additional CSS classes applied to both mask halves
classNamesElementSlots<WheelPickerMaskSlots>-Additional CSS classes for individual mask slots
stylesPartial<Record<WheelPickerMaskSlots, ViewStyle>>-Inline styles for individual mask slots
...ViewPropsOmit<ViewProps, 'children'>-All standard React Native View props are supported

ElementSlots<WheelPickerMaskSlots>

slotdescription
topTop fade overlay
bottomBottom fade overlay

styles

slottypedescription
topViewStyleInline style for the top fade overlay
bottomViewStyleInline style for the bottom fade overlay

WheelPicker.Item

WheelPicker.Item is the per-row animated container. When no children are provided, it auto-renders <WheelPicker.ItemLabel>{item.label}</WheelPicker.ItemLabel>. Tapping the row scrolls the wheel to focus it; a consumer onPress runs first.

proptypedefaultdescription
childrenReact.ReactNode-Custom row content. When omitted, the row renders WheelPicker.ItemLabel with the option's label
classNamestring-Additional CSS classes for the row container (see animated property notes below)
styleViewStyle-Inline style for the row container. Merged after the root styles.item cascade and the animated transform style
...PressablePropsOmit<PressableProps, 'children' | 'style'>-All standard React Native Pressable props are supported (onPressIn, hitSlop, disabled, etc.)

The row container animates opacity and transform (scale) for distance-based fade and scale. These properties cannot be overridden via className; use the animation prop on the root to customize, or animation="disabled" to remove them entirely.

WheelPicker.ItemLabel

proptypedefaultdescription
classNamestring-Additional CSS classes applied to the label text
...TextPropsTextProps-All standard React Native Text props are supported

Hooks

useWheelPicker

Hook to access the WheelPicker root context. Must be used within a WheelPicker component.

import { useWheelPicker } from 'heroui-native-pro';

const { itemHeight, visibleCount, scrollY, isDisabled } = useWheelPicker();

Returns: WheelPickerContextValue

propertytypedescription
itemHeightnumberResolved row height in pixels
visibleCountnumberNumber of visible rows (always odd)
isDisabledbooleanWhether the wheel is disabled
scrollYSharedValue<number>Shared scroll offset (UI thread) driving per-item animations
isInsideGroupbooleanWhether a WheelPickerGroup parent provided the value
resolvedAnimationWheelPickerResolvedAnimationConfigResolved [edge, center] opacity and scale ranges
isItemAnimationDisabledbooleanWhether per-item animation is disabled (own + cascade)
scrollToIndex(params: { index: number; animated?: boolean }) => voidImperative helper to scroll the wheel to a row index

useWheelPickerItem

Hook to access the per-row item context. Must be used within a WheelPicker.Item, WheelPicker.ItemLabel, or any component rendered inside a custom renderItem. The context erases the generic to unknown; cast at the call site (as WheelPickerItemRenderProps<MyType>) when you need strict typing on item.value.

import { useWheelPickerItem } from 'heroui-native-pro';

const { item, index, isSelected, absDistance } = useWheelPickerItem();

Use absDistance together with useAnimatedStyle / useAnimatedProps to build your own row-content animations (icons, badges, halos, indicators, etc.) without recomputing distance math yourself.

Returns: WheelPickerItemRenderProps

propertytypedescription
itemWheelPickerOption<unknown>The option being rendered
indexnumberZero-based index of the option in items
isSelectedbooleanWhether the option matches the current value
absDistanceSharedValue<number>Per-row absolute distance from center, in row units (0 = centered, 0.5 = selection boundary, 1+ = one full row or more away). Read with .get() inside a worklet

On this page