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

@shaquillehinds/react-native-dropdown-selector

v0.0.8

Published

A simple dropdown selector for react native that just works.

Readme

@shaquillehinds/react-native-dropdown-selector

An animated dropdown selector for React Native that measures its position on screen and expands upward or downward depending on available space. Every part is styleable, and the whole thing is generic over your value type.


Contents


Installation

npm install @shaquillehinds/react-native-dropdown-selector

Peer dependencies:

npm install react-native-gesture-handler react-native-reanimated react-native-svg

@shaquillehinds/react-native-essentials provides the layout and animation primitives.

Import react-native-gesture-handler at the top of your entry file and wrap your app in GestureHandlerRootView — the dropdown list scrolls with a gesture-handler ScrollView.


Quick start

import { useState } from 'react';
import { DropDownSelector } from '@shaquillehinds/react-native-dropdown-selector';

const FRUITS = [
  { label: 'Apple', value: 'apple' },
  { label: 'Banana', value: 'banana' },
  { label: 'Orange', value: 'orange' },
];

function FruitPicker() {
  const [fruit, setFruit] = useState('apple');

  return (
    <DropDownSelector
      items={FRUITS}
      selectedItem={fruit}
      onSelect={setFruit}
      placeholder="Select a fruit"
    />
  );
}

The component is fully controlled — there is no internal selection state and no default value. items, selectedItem, onSelect and placeholder are all required.

T is inferred from items and selectedItem; annotate explicitly (<DropDownSelector<number> …>) only where inference fails.

Keep items referentially stable — a module constant or useMemo — since it feeds a memoised label lookup.


Items and values

type DropDownItem<T> = { label: string; value: T };

Values are compared with ===

The displayed label and the selected indicator are both resolved by item.value === selectedItem. Use primitives — string or number.

If you use an object or array as a value, the same reference must appear in both items and selectedItem. A fresh object literal on each render silently breaks the match: the placeholder shows instead of the label, and no item is marked selected.

// ❌ new references every render — never matches
items={users.map(u => ({ label: u.name, value: { id: u.id } }))}

// ✅ select by id and look the object up yourself
items={users.map(u => ({ label: u.name, value: u.id }))}
selectedItem={selectedUserId}
onSelect={setSelectedUserId}

Labels must be unique

Items are keyed by label, not by value. Two items sharing a label cause a React key collision. Disambiguate the label text rather than relying on distinct values.

Showing the placeholder

There is no "unselected" sentinel. Pass a selectedItem that matches nothing ('', null, -1) and the placeholder shows.


Direction and size

Automatic direction

Leave expandDirection unset and the component measures its position on screen, compares the distance to the bottom against the dropdown's maximum height, and expands upward when there isn't room below. The chevron animates to match.

Forcing a direction

expandDirection = 'up'; // always upward
expandDirection = 'down'; // always downward

expandDistance

Maximum dropdown height in pixels. Defaults to 30% of screen height in column layout and 20% in row layout. Content taller than this scrolls.

This value also feeds the direction decision — a larger expandDistance makes upward expansion more likely, since more space is required below.


Layout modes

dropDownItemsLayout="column" (default)

Items stack vertically and the list scrolls vertically past expandDistance. The selected item shows a radio indicator on the right.

dropDownItemsLayout="row"

Items lay out horizontally and scroll horizontally; vertical scrolling is disabled. The selected item shows a small dot above its label. Default max height drops to 20% of screen height.

Suited to short labels — filter chips, sort options, day pickers.


Styling

Each visual part has its own prop rather than a single style object:

| Prop | Targets | Defaults worth knowing | | ---------------------------------- | ------------------ | ------------------------------------------------------------------ | | containerProps | Root wrapper | zIndex: maxZIndex | | dropdownButtonProps | The button | White, borderRadius: 'medium', padding [1, 5], always shadowed | | dropdownButtonTextProps | Button label | 1 line, maxWidth: '90%' | | dropdownButtonIconContainerProps | Chevron wrapper | – | | dropdownButtonIconProps | Built-in chevron | { size?, color? }, black | | dropdownScrollViewProps | Scroll container | – | | dropdownContentContainerProps | Item list | #FAFAFA, borderRadius: 'soft' | | dropdownItemProps | Each item row | – | | dropdownItemTextProps | Each item label | 1 line | | DropdownSelectedItemIconColor | Selected indicator | – |

These take react-native-essentials layout props — backgroundColor, borderRadius: 'soft' | 'medium' | 'large', padding: [vertical, horizontal], margin, and so on — not raw style objects. Most also accept style, which merges last.

<DropDownSelector
  items={items}
  selectedItem={value}
  onSelect={setValue}
  placeholder="Select option"
  dropdownButtonProps={{
    backgroundColor: '#1E1E1E',
    borderRadius: 'large',
    padding: [3, 5],
  }}
  dropdownButtonTextProps={{ color: '#fff', fontSize: 16, fontWeight: '500' }}
  dropdownContentContainerProps={{ backgroundColor: '#2A2A2A' }}
  dropdownItemProps={{ padding: [3, 5], backgroundColor: '#2A2A2A' }}
  dropdownItemTextProps={{ color: '#fff', fontSize: 15 }}
  DropdownSelectedItemIconColor="#007AFF"
/>

Don't override padding on dropdownContentContainerProps

The list carries a direction-dependent padding — top when expanding down, bottom when expanding up — that keeps it clear of the button. Overriding padding removes that clearance and the list sits on top of the button.

Space the items with dropdownItemProps.padding, or set margin on the container, instead.

disableShadow covers the list only

The button is shadowed unconditionally. To remove that, override through dropdownButtonProps.style, which merges after the default.

On Android the list shadow is an animated overlay rather than a static style; disableShadow removes both paths.

Row layout ignores some container props

In 'row' layout, flexDirection, scrollable and horizontal are applied after your dropdownContentContainerProps and can't be overridden.


Custom components

DropdownItemSelectedIcon

Replaces just the selected indicator, leaving the row's press handling and layout intact. This is the lightest way to customize.

DropdownItemSelectedIcon={({ item }) => <CheckIcon color="#007AFF" />}

DropdownButtonIcon

Replaces the chevron.

DropdownButtonIcon={({ isOpen, expandDirection }) => (
  <Chevron
    style={{ transform: [{ rotate: isOpen ? '180deg' : '0deg' }] }}
    pointing={expandDirection}
  />
)}

isOpen reflects the dropdown state. expandDirection is 'up' or 'down' and matches the direction the list actually expands, whether forced or automatic.

The built-in chevron doesn't render until the component has measured its position, so there's one frame without it. A custom icon renders immediately — worth using if that frame is noticeable in your layout.

DropdownItemComponent

Replaces the entire item row. Your component receives four props:

| Prop | Type | Description | | ------------ | ----------------- | ------------------------------------------------------------------ | | item | DropDownItem<T> | This row's label and value | | isSelected | boolean | item.value === selectedItem | | onPress | () => void | Selects the item, fires onDropdownItemPress, closes the dropdown | | close | () => void | Closes without selecting |

DropdownItemComponent={({ item, isSelected, onPress }) => (
  <Pressable onPress={onPress} style={styles.row}>
    <Image source={{ uri: item.icon }} style={styles.icon} />
    <Text style={[styles.label, isSelected && styles.selected]}>{item.label}</Text>
    {isSelected && <CheckIcon />}
  </Pressable>
)}

Because your component replaces the touchable wrapper, it has to call onPress itself. An item that never calls it renders correctly and does nothing.

Don't call onSelect directly from inside a custom item — onPress already does that and closes the dropdown as well.

For anything short of a full row redesign, prefer dropdownItemProps, dropdownItemTextProps and DropdownItemSelectedIcon.


Open and close behaviour

  • The button toggles the dropdown.
  • Selecting an item closes it — via the built-in row, or via onPress in a custom one.
  • Tapping outside does not close it. There's no dismissing backdrop; taps pass through to the content behind.

isDisabled disables the button; it doesn't close an already-open dropdown.

There is no ref or imperative API — the dropdown can't be opened or closed programmatically.

Callbacks

onOpen={() => analytics.track('dropdown_opened')}
onClose={() => analytics.track('dropdown_closed')}
onDropdownItemPress={item => console.log('picked', item.label)}

onOpen fires when the list mounts, onClose when it unmounts — after the unmount delay. onDropdownItemPress fires right after onSelect.


Animation

expandAnimationConfig={{ type: 'spring', stiffness: 200, damping: 20, useNativeDriver: true }}
expandAnimationConfig={{ type: 'timing', duration: 250, useNativeDriver: true }}

This is React Native's Animated config shape, not Reanimated's. Spring takes stiffness/damping or speed/bounciness; timing takes duration and easing. Omit toValue — it's supplied internally.

Always include useNativeDriver. The animation drives opacity and translateY, so true is correct.

The default is a spring with stiffness: 200, damping: 20.

Unmount delay

The list stays mounted briefly after closing so the exit animation can play. The delay resolves as:

  • timing expandAnimationConfig → its duration (falling back to 300)
  • otherwise → unMountDelayInMilliSeconds (falling back to 300)

So a timing config silently ignores unMountDelayInMilliSeconds. Pair unMountDelayInMilliSeconds with a spring config, or just set the timing duration.

If the dropdown snaps away before finishing its exit, the delay is shorter than the animation.


API reference

| Prop | Type | Required | Description | | ---------------------------------- | ------------------------------------------------------- | -------- | ---------------------------------------------------- | | items | DropDownItem<T>[] | ✅ | { label, value }; labels must be unique | | selectedItem | T | ✅ | Matched against item.value with === | | onSelect | (value: T) => void | ✅ | Called on selection | | placeholder | string | ✅ | Shown when no item matches | | onOpen | () => void | | List mounted | | onClose | () => void | | List unmounted, after the delay | | onDropdownItemPress | (item: DropDownItem<T>) => void | | Fires after onSelect | | isDisabled | boolean | | Disables the button | | disableShadow | boolean | | List shadow only | | dropDownItemsLayout | 'column' \| 'row' | | Default 'column' | | expandDirection | 'up' \| 'down' | | Omit for automatic | | expandDistance | number | | Max height in px; default 30% / 20% of screen height | | expandAnimationConfig | RN Animated timing or spring config + type | | Overrides the default spring | | unMountDelayInMilliSeconds | number | | Default 300; ignored with a timing config | | containerProps | LayoutProps | | Root wrapper | | dropdownButtonProps | RNPressableLayoutProps | | The button | | dropdownButtonTextProps | BaseTextProps | | Button label | | dropdownButtonIconContainerProps | LayoutProps | | Chevron wrapper | | dropdownButtonIconProps | { size?: number; color?: string } | | Built-in chevron | | DropdownButtonIcon | ({ isOpen, expandDirection }) => JSX.Element | | Replaces the chevron | | dropdownScrollViewProps | ScrollViewProps | | Scroll container | | dropdownContentContainerProps | LayoutProps | | Item list; don't override padding | | DropdownItemComponent | ({ item, isSelected, onPress, close }) => JSX.Element | | Replaces the row; must call onPress | | dropdownItemProps | TouchableLayoutProps | | Item row | | dropdownItemTextProps | BaseTextProps | | Item label | | DropdownItemSelectedIcon | ({ item }) => JSX.Element | | Replaces the indicator only | | DropdownSelectedItemIconColor | string | | Built-in indicator colour |

Types

type DropDownItem<T> = { label: string; value: T };

type DropdownButtonIconProps = { size?: number; color?: string };

type ExpandAnimationConfig =
  | (Omit<Animated.TimingAnimationConfig, 'toValue'> & { type: 'timing' })
  | (Omit<Animated.SpringAnimationConfig, 'toValue'> & { type: 'spring' });

Exports

import DropDownSelector, {
  DropDownSelector,
} from '@shaquillehinds/react-native-dropdown-selector';

import type {
  DropDownItem,
  DropDownItemValue,
  DropDownSelectorProps,
  DropdownButtonIconProps,
} from '@shaquillehinds/react-native-dropdown-selector';

Named and default export are the same component.


Recipes

Selecting by id

const [userId, setUserId] = useState<number>(1);

const items = useMemo(
  () => users.map((u) => ({ label: u.name, value: u.id })),
  [users]
);

<DropDownSelector
  items={items}
  selectedItem={userId}
  onSelect={setUserId}
  placeholder="Select a user"
/>;

The selected user object is users.find(u => u.id === userId). Keep objects out of value so === keeps working.

Filter chips

<DropDownSelector
  items={SORT_OPTIONS}
  selectedItem={sort}
  onSelect={setSort}
  placeholder="Sort"
  dropDownItemsLayout="row"
  expandDistance={120}
  dropdownItemTextProps={{ fontSize: 14 }}
/>

Rich item rows

<DropDownSelector
  items={countries}
  selectedItem={countryCode}
  onSelect={setCountryCode}
  placeholder="Select a country"
  DropdownItemComponent={({ item, isSelected, onPress }) => (
    <Pressable onPress={onPress} style={styles.countryRow}>
      <Flag code={item.value} />
      <Text style={[styles.name, isSelected && styles.selectedName]}>
        {item.label}
      </Text>
      {isSelected && <CheckIcon color="#007AFF" />}
    </Pressable>
  )}
/>

Dark theme

<DropDownSelector
  items={items}
  selectedItem={value}
  onSelect={setValue}
  placeholder="Select option"
  dropdownButtonProps={{
    backgroundColor: '#1E1E1E',
    borderRadius: 'large',
    borderWidth: 1,
    borderColor: '#333',
  }}
  dropdownButtonTextProps={{ color: '#fff' }}
  dropdownButtonIconProps={{ color: '#fff' }}
  dropdownContentContainerProps={{ backgroundColor: '#2A2A2A' }}
  dropdownItemProps={{ backgroundColor: '#2A2A2A' }}
  dropdownItemTextProps={{ color: '#fff' }}
  DropdownSelectedItemIconColor="#007AFF"
/>

Inside a scroll view

Position is measured on layout and re-read when the button is pressed, so scrolling between renders is handled. If the dropdown consistently picks the wrong direction inside a container that shifts after mount, force it with expandDirection.


Troubleshooting

The label never shows — always the placeholder. selectedItem isn't matching any item.value under ===. Usually an object or array value with a fresh reference each render. Switch to a primitive.

Nothing is marked selected. Same cause as above.

Items render oddly or reorder. Duplicate label values — items are keyed by label.

A custom item doesn't select or close. Your DropdownItemComponent isn't calling the onPress it receives.

Tapping outside doesn't dismiss it. Expected; there's no dismissing backdrop.

The list covers the button. padding was overridden on dropdownContentContainerProps, removing the direction-dependent clearance.

The exit animation is cut short. The unmount delay is shorter than the animation. Raise unMountDelayInMilliSeconds, or use a timing config whose duration covers it.

unMountDelayInMilliSeconds seems ignored. It is, when expandAnimationConfig is a timing config — the config's duration wins.

Items don't scroll. Confirm react-native-gesture-handler is imported in your entry file and GestureHandlerRootView wraps the app. In 'row' layout vertical scrolling is disabled by design.

The button shadow won't go away. disableShadow only covers the list. Override through dropdownButtonProps.style.


AI agent rules

The package ships a rules file written for AI coding agents (Claude Code, Cursor, Codex, Copilot, etc.) at rules/AGENT_RULES.md. It spells out the behaviour that isn't visible from the types — that values are matched with === and items keyed by label, that a custom item component has to call its own onPress, that tapping outside doesn't close the dropdown — and lists every prop so it cannot invent APIs. Point your agent at it with any of the following.

Copy it into your project (recommended)

npx rndd-rules            # writes ./AGENTS.md
npx rndd-rules cursor     # writes ./.cursor/rules/react-native-dropdown-selector.mdc (alwaysApply)
npx rndd-rules claude     # writes ./.claude/rules/react-native-dropdown-selector.md
npx rndd-rules codex      # writes ./.codex/rules/react-native-dropdown-selector.md
npx rndd-rules copilot    # writes ./.github/instructions/react-native-dropdown-selector.instructions.md
npx rndd-rules windsurf   # writes ./.windsurf/rules/react-native-dropdown-selector.md
npx rndd-rules docs/ai/dropdown.md   # custom path

Add --force to overwrite an existing file. --print writes the rules to stdout instead of to disk. Re-run after upgrading the package to pick up rule changes.

Reference it without copying (Claude Code)

CLAUDE.md supports @path imports, so a single line keeps the rules in sync with the installed version:

# CLAUDE.md

@node_modules/@shaquillehinds/react-native-dropdown-selector/rules/AGENT_RULES.md

Reference it from a generic AGENTS.md

Before adding or customizing a dropdown, read and follow
node_modules/@shaquillehinds/react-native-dropdown-selector/rules/AGENT_RULES.md.

License

MIT © Shaquille Hinds