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

react-native-smart-dropdown

v0.1.3

Published

Production-ready, reusable React Native dropdown/select with smart positioning, search, and multi-select. Zero runtime dependencies.

Readme

react-native-smart-dropdown

npm version

Production-ready React Native dropdown / select with smart positioning, search, multi-select, and a full theming system. Zero runtime dependencies. TypeScript-first.

npm · GitHub · Issues · Interactive demo · Full video tour

Preview

Highlights from the interactive demo on Android and iOS:

Android

| Selection (single + multi) | Multi-select · searchable | Inline + keyboard-aware | |:---:|:---:|:---:| | Single & multi-select | Searchable multi-select chips | Inline mode with search |

iOS

| Create new / taggable | Multi-select · searchable | |:---:|:---:| | Create new option on iOS | Multi-select chips on iOS |

Watch all 32 demos on YouTube — full walkthrough of every feature in the demo app.

Features

  • Single & multi-select — controlled or uncontrolled; multi shows a count or removable chips (with +N more overflow).
  • Searchlocal, remote (debounced + race-safe), or hybrid; optional pagination (infinite scroll) and load-on-open.
  • Autocomplete — trigger-as-search combobox; create-new / taggable items.
  • Smart positioning — auto up/down, keyboard-aware, edge-clamped; floating Modal or inline (in-flow) render.
  • Rich rows — section grouping, disabled items, subtitle + avatar, fully custom renderItem.
  • Formslabel / required / helperText / error (reddens the border); minSelections / maxSelections + validity callback.
  • Theming — partial theme override, light/dark (colorScheme), custom fonts (iOS-safe, family-based), RTL, custom icons.
  • ControlrenderTrigger, controlled open, imperative ref, animated open/close.
  • Scale & web — virtualized inline lists, getItemLayout, keyboard navigation (↑/↓/Enter/Esc) + hover.

Install

npm install react-native-smart-dropdown
# or
yarn add react-native-smart-dropdown

Peer deps: react >= 18, react-native >= 0.72. MIT license · 90 tests · ~75 kB packed · zero runtime dependencies.

Interactive demo (32 cards)

The repo includes DropdownDemo32 runnable cards, one feature per card:

| Category | Demos | |----------|------:| | SELECTION | 7 | | SEARCH | 7 | | LIST CONTENT | 4 | | FORM FIELD | 1 | | THEMING & ICONS | 5 | | POSITIONING & RENDER | 7 | | STATES | 1 | | Total | 32 |

Run locally from example/DropdownDemo, or watch the full video tour on YouTube.

Quick start

import React from 'react';
import { DropdownSelect } from 'react-native-smart-dropdown';

type Fruit = { id: number; label: string };
const FRUITS: Fruit[] = [
  { id: 1, label: 'Apple' },
  { id: 2, label: 'Banana' },
];

function Example() {
  const [value, setValue] = React.useState<Fruit | null>(null);
  return (
    <DropdownSelect<Fruit>
      items={FRUITS}
      labelKey="label"
      valueKey="id"
      value={value}
      onChange={setValue}
      placeholder="Pick a fruit"
    />
  );
}

Recipes

Multi-select · chips + search

<DropdownSelect<Fruit>
  mode="multi"
  multiDisplay="chips"
  isSearch
  searchInTrigger
  searchMode="local"
  items={FRUITS}
  labelKey="label"
  valueKey="id"
  selectedValues={selected}
  onChangeMulti={setSelected}
  placeholder="Type to search, pick several"
/>

Search query clears after each pick by default. Pass persistSearchOnSelect to keep it.

Multi-select · chips + search (with cap)

<DropdownSelect<Fruit>
  mode="multi"
  multiDisplay="chips"
  maxVisibleChips={3}        // extras collapse into "+N more"
  isSearch
  searchInTrigger
  searchMode="local"
  items={FRUITS}
  labelKey="label"
  valueKey="id"
  selectedValues={selected}
  onChangeMulti={setSelected}
  placeholder="Pick several"
/>

Select-all · min/max validation

<DropdownSelect<Fruit>
  mode="multi"
  multiDisplay="chips"
  showSelectAll               // header toggles Select all ↔ Clear all
  minSelections={1}
  maxSelections={3}
  onValidityChange={setValid}
  items={FRUITS} labelKey="label" valueKey="id"
  selectedValues={sel} onChangeMulti={setSel}
/>

Remote search · pagination · load on open

<DropdownSelect<Item>
  isSearch
  searchInTrigger
  searchMode="remote"
  loadOnOpen                  // fetch first page when opened
  minSearchLength={0}
  enablePagination
  pageSize={20}
  fetchData={(text, page) => api.search(text, page)}
  renderLoading={() => <MySkeleton />}
  items={[]} labelKey="label" valueKey="id"
/>

Hybrid search (local + remote)

<DropdownSelect<Item>
  isSearch
  searchInTrigger
  searchMode="hybrid"           // local matches first, remote merged in
  minSearchLength={1}
  fetchData={(text, page) => api.search(text, page)}
  items={LOCAL_ITEMS}             // seed list for instant local matches
  labelKey="label" valueKey="id"
/>

Create new / taggable

<DropdownSelect<Item>
  isSearch searchInTrigger searchMode="local"
  allowCreate
  onCreateOption={(text) => ({ id: Date.now(), label: text })}
  items={items} labelKey="label" valueKey="id"
  value={value} onChange={setValue}
/>

Grouped · disabled · subtitle + avatar

<DropdownSelect<User>
  items={users} labelKey="name" valueKey="id"
  groupBy={(u) => u.team}
  isItemDisabled={(u) => !u.active}
  descriptionKey={(u) => u.email}
  imageKey={(u) => u.avatarUrl}
/>

Form field (label / required / error)

<DropdownSelect<Fruit>
  label="Favorite fruit"
  required
  error={value ? undefined : 'This field is required'}
  items={FRUITS} labelKey="label" valueKey="id"
  value={value} onChange={setValue}
/>

Theming · dark · custom font · RTL

<DropdownSelect<Fruit>
  theme={{ colors: { accent: '#0a7', chipBg: '#0a7' }, radii: { md: 14 } }}
  colorScheme="dark"                          // 'system' | 'light' | 'dark'
  // iOS-safe: family-only per weight (NO fontWeight when a custom family is set)
  // theme={{ fonts: { regular: 'Inter-Regular', bold: 'Inter-Bold' } }}
  rtl                                         // mirror layout + right-align (Arabic/Hebrew)
  items={FRUITS} labelKey="label" valueKey="id"
/>

Fonts on iOS: iOS ignores fontWeight on a custom fontFamily — the weight is baked into the font file. So set per-weight families (Inter-Regular / Inter-Medium / Inter-Bold) via theme.fonts; the library never emits fontWeight alongside a custom family.

Custom trigger · controlled open

import { Pressable, Text } from 'react-native';

<DropdownSelect<Fruit>
  open={open}
  onOpenChange={setOpen}
  renderTrigger={({ toggle, label, placeholder, isOpen }) => (
    <Pressable onPress={toggle}>
      <Text>{label || placeholder} {isOpen ? '▲' : '▼'}</Text>
    </Pressable>
  )}
  items={FRUITS} labelKey="label" valueKey="id"
/>

Inline · virtualized (large lists)

<DropdownSelect<Item>
  inline
  scrollRef={scrollRef}        // the host ScrollView ref
  virtualizeInline             // windowed FlatList for big inline lists
  itemHeight={44}              // enables getItemLayout (uniform rows)
  items={bigList} labelKey="label" valueKey="id"
/>

Full prop reference

Every prop on <DropdownSelect<T>>, grouped. Optional unless marked required. See the DropdownDemo app for 32 runnable demos (one per feature).

Data

| Prop | Type | Default | Notes | |------|------|---------|-------| | items | T[] | — | required. Option list. | | labelKey | keyof T \| (item: T) => string | tries label/name | how to read the display label | | valueKey | keyof T \| (item: T) => string \| number | tries value/id | how to read the unique id | | keyExtractor | (item, index) => string | from valueKey | list key override | | descriptionKey | keyof T \| (item: T) => string | — | secondary line (subtitle) per row | | imageKey | keyof T \| (item: T) => string | — | left avatar image URI per row | | renderRowLeft | (item) => ReactNode | — | custom left accessory per row (overrides imageKey) |

Selection

| Prop | Type | Default | Notes | |------|------|---------|-------| | mode | 'single' \| 'multi' | 'single' | | | value / onChange | T \| null / (item\|null) => void | — | single, controlled | | defaultValue | T \| null | — | single, uncontrolled | | selectedValues / onChangeMulti | T[] / (items) => void | — | multi, controlled | | defaultSelectedValues | T[] | — | multi, uncontrolled | | multiDisplay | 'count' \| 'chips' | 'count' | multi trigger display | | maxVisibleChips | number | — | cap chips → "+N more" pill | | maxSelections | number | — | cap multi selections | | minSelections | number | — | block deselect below this | | onValidityChange | (valid: boolean) => void | — | fires when min/max/required validity changes | | includeSelectedInList | boolean | true | inject selected value not present in items | | closeOnSelect | boolean | single true / multi false | close after a pick |

Search

| Prop | Type | Default | Notes | |------|------|---------|-------| | isSearch | boolean | false | enable search | | searchMode | 'none' \| 'local' \| 'remote' \| 'hybrid' | 'none' | | | searchInTrigger | boolean | false | trigger itself is the search input (autocomplete) | | searchPlaceholder | string | 'Search…' | | | autoFocusSearch | boolean | false | focus search on open | | minSearchLength | number | 0 | min chars before remote fetch | | searchDebounceMs | number | 400 | remote debounce | | filterFunction | (item, text) => boolean | substring on label | local filter override | | onSearchTextChange | (text) => void | — | observe query | | fetchData | (text, page?) => Promise<T[]> | — | remote/hybrid fetch | | enablePagination | boolean | false | infinite scroll (remote/hybrid) | | pageSize | number | — | page size for last-page detection | | loadOnOpen | boolean | false | fetch first page on open | | persistSearchOnSelect | boolean | false | keep query after a pick |

List content

| Prop | Type | Default | Notes | |------|------|---------|-------| | groupBy | (item) => string | — | section grouping | | renderSectionHeader | (group) => ReactNode | styled label | custom section header | | isItemDisabled | (item) => boolean | — | greyed, non-selectable rows | | allowCreate | boolean | false | show "Add …" for unmatched query | | onCreateOption | (text) => T | — | build a new item (required for allowCreate) | | createOptionLabel | (text) => string | Add "<text>" | create-row label | | showSelectAll | boolean | false | select-all / clear-all header (multi) | | selectAllText / clearAllText | string | 'Select all' / 'Clear all' | | | highlightSelected | boolean | true | highlight selected rows | | autoScrollToSelected | boolean | true | scroll to selection on open |

Form field

| Prop | Type | Default | Notes | |------|------|---------|-------| | label | string | — | label above the trigger | | required | boolean | false | adds a * | | helperText | string | — | helper below (hidden when error set) | | error | string | — | error text + reddens the trigger border | | labelStyle / helperTextStyle | StyleProp<TextStyle> | — | |

Trigger appearance & icons

| Prop | Type | Default | Notes | |------|------|---------|-------| | placeholder | string | 'Select…' | | | disabled | boolean | false | | | triggerStyle / triggerTextStyle / placeholderStyle | StyleProp | — | | | chipStyle / chipTextStyle | StyleProp | — | multi chips | | caretUpIcon / caretDownIcon / clearIcon / leftIcon | ReactNode | ▲ ▼ ✕ | any node (svg/Image/vector/text) | | leftIconStyle | StyleProp<ViewStyle> | — | left-icon slot | | showRightAccessory | boolean | true | show the caret | | isClearable | boolean | true | show the clear ✕ | | showClearOnlyWhenHasValue | boolean | true | hide clear when empty | | openOnClear | boolean | false | reopen after clearing |

Theming

| Prop | Type | Default | Notes | |------|------|---------|-------| | theme | DeepPartial<DropdownTheme> | — | partial token override (see below) | | colorScheme | 'system' \| 'light' \| 'dark' | 'system' | base palette | | rtl | boolean | I18nManager.isRTL | mirror layout + right-align text |

Positioning & sizing

| Prop | Type | Default | Notes | |------|------|---------|-------| | direction | 'up' \| 'down' \| 'auto' | 'auto' | open direction | | allowDirectionFallback | boolean | true | flip if the chosen side lacks space | | listHeight | number | 280 | requested panel height | | minListHeight / maxListHeight | number | 0 / — | clamp | | autoAdjustHeight | boolean | true | shrink/grow to available space | | shrinkToContent | boolean | true | fit to actual content height | | edgeMargin | number | 8 | min gap from screen edges |

Render mode & behavior

| Prop | Type | Default | Notes | |------|------|---------|-------| | inline | boolean | false | in-flow panel instead of floating Modal | | scrollRef | RefObject | — | host ScrollView ref (inline keyboard-reveal) | | virtualizeInline | boolean | false | windowed FlatList for big inline lists | | itemHeight | number | — | fixed row height → getItemLayout | | animated | boolean | false | LayoutAnimation on open/close + chips | | open / onOpenChange | boolean / (open: boolean) => void | — | controlled open state | | showOverlay | boolean | false | dim backdrop when true (Modal mode) | | overlayColor | string | theme overlay | backdrop color | | closeOnOutsidePress | boolean | true | tap-outside closes | | initialNumToRender | number | 10 | FlatList perf | | listProps | Partial<FlatListProps<T>> | — | passthrough to the list |

States & render slots

| Prop | Type | Default | Notes | |------|------|---------|-------| | loading | boolean | false | parent-controlled loading | | emptyText / searchEmptyText / errorText / minSearchText | string | see defaults | empty/error copy | | showEmptyAction / emptyActionLabel / onEmptyActionPress | boolean / string / () => void | — | empty-state action button | | emptyContainerStyle / emptyTextStyle / emptyActionStyle / emptyActionTextStyle | StyleProp | — | | | renderEmptyState | (params: EmptyStateParams) => ReactNode | — | fully custom empty state | | renderLoading | () => ReactNode | spinner | custom loader | | renderTrigger | (params: RenderTriggerParams<T>) => ReactNode | — | fully custom trigger | | renderItem | (params: RenderItemParams<T>) => ReactNode | — | fully custom row | | renderLeftAccessory / renderRightAccessory / renderClearAccessory | (params: AccessoryParams<T>) => ReactNode | — | accessory slots (win over icon props) |

Accessibility / test

| Prop | Type | Notes | |------|------|-------| | accessibilityLabel | string | trigger a11y label | | testID | string | base testID (children derive <testID>-row-N, -search, -chip-N-remove, …) |

Theme tokens (DropdownTheme)

Override any subset via the theme prop.

{
  colors: { surface, text, textMuted, placeholder, inputPlaceholder, border, divider,
            chipBg, onChip, accent, error, overlay, triggerPressed, rowSelected,
            rowPressed, ripple, shadow },
  radii:     { sm, md },
  spacing:   { xs, sm, md, lg },
  fontSizes: { xs, sm, chip, body, input },
  fonts:     { regular?, medium?, bold? },   // family-only; iOS-safe (no fontWeight when set)
  sizes:     { control, row, input },        // auto-scaled 1.2× on tablets/iPad
  rtl: boolean,
}

Public exports

// Component
DropdownSelect

// Types
DropdownSelectProps, DropdownRef, Direction, ResolvedDirection,
SelectionMode, SearchMode, DataStatus,
RenderTriggerParams, RenderItemParams, AccessoryParams, EmptyStateParams

// Theming
lightTheme, darkTheme, mergeTheme, fontFor, textDir, rowDir
DropdownTheme, DropdownColors, DropdownFonts, DeepPartial

// Host integration (ScrollView / outside-tap coordination)
closeOpenDropdowns, markUserTap, subscribeOpen

Host integration (inline + ScrollView)

When using inline mode inside a host ScrollView, wire these helpers so outside taps close the dropdown and you can react to open/close (e.g. lock page scroll while open):

import React from 'react';
import { ScrollView } from 'react-native';
import {
  DropdownSelect,
  markUserTap,
  subscribeOpen,
  closeOpenDropdowns,
} from 'react-native-smart-dropdown';

function FormScreen() {
  const scrollRef = React.useRef<ScrollView>(null);
  const [dropdownOpen, setDropdownOpen] = React.useState(false);

  React.useEffect(() => subscribeOpen(setDropdownOpen), []);

  return (
    <ScrollView
      ref={scrollRef}
      scrollEnabled={!dropdownOpen}
      onScrollBeginDrag={closeOpenDropdowns}
      onTouchStart={markUserTap}
      keyboardShouldPersistTaps="handled">
      <DropdownSelect<Fruit>
        inline
        scrollRef={scrollRef}
        items={FRUITS}
        labelKey="label"
        valueKey="id"
      />
    </ScrollView>
  );
}
  • markUserTap — call on the host root onTouchStart; closes inline dropdowns on outside tap (Modal dropdowns use their own backdrop).
  • subscribeOpen — fires (open: boolean) whenever any dropdown opens or closes.
  • closeOpenDropdowns — imperatively close the open dropdown (e.g. on host scroll).

Only one dropdown stays open at a time — opening another closes the previous one automatically.

Render-prop params

RenderTriggerParams<T> = { isOpen, disabled, hasValue, value, selectedValues,
                           label, placeholder, open, close, toggle, clear }
RenderItemParams<T>    = { item, index, isSelected, onPress }
AccessoryParams<T>     = { hasValue, isOpen, disabled, value, selectedValues }
EmptyStateParams       = { status, searchText, onActionPress? }

Imperative API (ref)

const ref = useRef<DropdownRef>(null);
ref.current?.open();
// DropdownRef: open · close · toggle · clear · focusSearch · blurSearch · remeasure · scrollToSelected

Keyboard & accessibility

Arrow ↑/↓ move the active row, Enter selects, Esc closes (while a search input is focused). Rows expose accessibilityRole / accessibilityState; web gets hover highlighting.

License

MIT · Bhavesh Barot · npm