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

@elberpg/ex-select-modal

v2.1.0

Published

Composable scroll-based column selector with modal shells for React Native / Expo

Readme

@elberpg/ex-select-modal

Composable scroll-column selector for React Native / Expo.
1, 2, or 3 independent drum-scroll columns — no native dependencies, no @elberpg/ex-modal required.

npm version license platform

Spanish docs: README_ES.md


Preview

| 1 Column | 2 Columns | 3 Columns | |:-:|:-:|:-:| | One column | Two columns | Three columns |

Fully themeable via global style objects — no props required.

| Dark ☕ | Square ⬛ | |:-:|:-:| | Dark select | Square select |


Installation

npm install @elberpg/ex-select-modal

Concept — composable pattern

The column picker and the modal are independent components. You compose them:

<SelectModal visible={open} onClose={onClose} onConfirm={() => onConfirm(draft)}>
  <Select columns={columns} value={draft} onChange={setDraft} />
</SelectModal>

This gives full style control over each part independently.


Select

Scroll-column picker with 1, 2, or 3 independent columns. Use standalone or inside any modal.

import { Select, SelectColumn } from '@elberpg/ex-select-modal';

const COL_QTY:  SelectColumn = { title: 'Qty',  items: ['1','2','3','4','5'], titleNull: '—' };
const COL_UNIT: SelectColumn = { title: 'Unit', items: ['kg', 'lb', 'g', 'oz'] };

<Select
  columns={[COL_QTY, COL_UNIT]}
  value={draft}
  onChange={setDraft}
/>

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | columns | SelectColumns | — | 1, 2, or 3 column definitions | | value | (string \| null)[] | — | Current value; one element per column | | onChange | (values: (string \| null)[], item?: SelectItem \| null) => void | — | Called on every scroll change |


SelectColumn

interface SelectColumn {
  title:       string;            // Label above the column
  items:       SelectItemInput[]; // string or { label, value, extra? }
  titleNull?:  string;            // Null option shown first (returns null when selected)
  renderItem?: (item: SelectItem, selected: boolean) => ReactNode; // Custom row render
}

Plain strings (simple case)

const COL_UNIT: SelectColumn = {
  title: 'Unit',
  items: ['kg', 'lb', 'g', 'oz'],
};

Label / value objects

Show a long label in the picker but store a short value in state:

const COL_UNIT: SelectColumn = {
  title: 'Unit',
  items: [
    { label: 'Kilograms', value: 'kg' },
    { label: 'Pounds',    value: 'lb' },
    { label: 'Grams',     value: 'g'  },
  ],
};

// onChange receives:
// values → ['kg']               short value — what you store
// item   → { label: 'Kilograms', value: 'kg' }  full object
<Select
  columns={[COL_UNIT]}
  value={draft}
  onChange={(values, item) => {
    setDraft(values);
    console.log(item?.label); // 'Kilograms'
  }}
/>

Extra metadata + renderItem

Use extra to attach any data (icons, symbols, flags…) and renderItem to fully control how each row looks:

const COL_CURRENCY: SelectColumn = {
  title: 'Currency',
  items: [
    { label: 'Dollar',  value: 'USD', extra: { icon: '🇺🇸', symbol: '$' } },
    { label: 'Euro',    value: 'EUR', extra: { icon: '🇪🇺', symbol: '€' } },
    { label: 'Peso MX', value: 'MXN', extra: { icon: '🇲🇽', symbol: '$' } },
  ],
  renderItem: (item, selected) => {
    const ex = item.extra as { icon: string; symbol: string };
    return (
      <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
        <Text>{ex.icon}</Text>
        <Text style={{ flex: 1, color: selected ? '#111' : '#999', fontWeight: selected ? '600' : '400' }}>
          {item.label}
        </Text>
        <Text style={{ color: selected ? '#2563eb' : '#bbb' }}>{ex.symbol}</Text>
      </View>
    );
  },
};

// onChange still gives you the plain value + full item:
<Select
  columns={[COL_CURRENCY]}
  value={draft}
  onChange={(values, item) => {
    setDraft(values);
    // values[0] → 'USD'
    // item.extra → { icon: '🇺🇸', symbol: '$' }
  }}
/>

SelectItem type

interface SelectItem {
  label:  string;   // Text shown in the column
  value:  string;   // Value stored in state
  extra?: unknown;  // Any metadata you need (icon, color, symbol…)
}

type SelectItemInput = string | SelectItem; // both forms are valid in items[]

titleNull — null option

const COL_SHADE: SelectColumn = {
  title: 'Shade',
  items: ['Light', 'Medium', 'Dark'],
  titleNull: 'None', // First option — column returns null when selected
};

SelectModal — native RN Modal

Bottom-sheet using React Native's Modal component.

import { Select, SelectModal } from '@elberpg/ex-select-modal';

const [open,  setOpen]  = useState(false);
const [val,   setVal]   = useState<(string | null)[]>([null, 'kg']);
const [draft, setDraft] = useState<(string | null)[]>([null, 'kg']);

<Pressable onPress={() => { setDraft(val); setOpen(true); }}>
  <Text>{val.filter(Boolean).join(' · ') || 'Select...'}</Text>
</Pressable>

<SelectModal
  visible={open}
  onClose={() => setOpen(false)}
  onConfirm={() => { setVal(draft); setOpen(false); }}
  title="Quantity and unit"
  clearable
  onClear={() => { setVal([null, null]); setOpen(false); }}
>
  <Select columns={[COL_QTY, COL_UNIT]} value={draft} onChange={setDraft} />
</SelectModal>

SelectModalOverlay — style-based overlay

Same API as SelectModal but uses position: absolute instead of RN's Modal.
Avoids conflicts with toasts, sheets, and other overlays. Must be placed at root level.

import { Select, SelectModalOverlay } from '@elberpg/ex-select-modal';

<SelectModalOverlay
  visible={open}
  onClose={() => setOpen(false)}
  onConfirm={() => { setVal(draft); setOpen(false); }}
  title="Quantity and unit"
>
  <Select columns={[COL_QTY, COL_UNIT]} value={draft} onChange={setDraft} />
</SelectModalOverlay>

Modal props — SelectModalProps

Shared by SelectModal and SelectModalOverlay.

| Prop | Type | Default | Description | |------|------|---------|-------------| | visible | boolean | — | Shows or hides the modal | | onClose | () => void | — | Called when the backdrop or Cancel is tapped | | onConfirm | () => void | — | Called when Confirm is tapped | | children | ReactNode | — | The <Select> to render inside | | title | string | — | Modal title | | confirmLabel | string | 'Confirm' | Confirm button text | | cancelLabel | string | 'Cancel' | Cancel button text | | clearable | boolean | false | Show a Clear button | | clearLabel | string | 'Clear' | Clear button text | | onClear | () => void | — | Called when Clear is tapped | | btnConfirm | ({ onPress }) => ReactNode | — | Replace the Confirm button | | btnCancel | ({ onPress }) => ReactNode | — | Replace the Cancel button | | btnClear | ({ onPress }) => ReactNode | — | Replace the Clear button |


onChange — parameter flow

items: [{ label: 'Kilograms', value: 'kg', extra: {...} }, ...]
         │
         │  user scrolls
         ▼
onChange(values, item)
  values  →  ['kg']                              one value per column (what you store)
  item    →  { label: 'Kilograms', value: 'kg' } the item that just changed (optional)
  • values[i] — the short value string (or null if titleNull is selected)
  • item — the full SelectItem that just changed; use it to read label or extra

Global styles

Two separate style objects — one for the picker columns, one for the modal shell.

ExSelect.styles — picker (scroll columns)

import { ExSelect } from '@elberpg/ex-select-modal';

ExSelect.styles.selectedColor   = '#0a84ff';
ExSelect.styles.unselectedColor = '#8e8e93';
ExSelect.styles.itemHeight      = 52;
ExSelect.styles.fontFamily      = 'Georgia';
ExSelect.styles.lineColor       = '#3a3a3c';
ExSelect.styles.columnTitle     = { fontSize: 11, color: '#888', textTransform: 'uppercase' };

| Field | Type | Description | |-------|------|-------------| | selectedColor | string | Color of the selected scroll item | | unselectedColor | string | Color of unselected scroll items | | itemHeight | number | Height of each scroll row in px | | fontFamily | string | Font family for scroll item text | | lineColor | string | Color of the two selection indicator lines | | columnTitle | TextStyle | Label above each scroll column |

ExSelectModal.styles — modal shell

import { ExSelectModal } from '@elberpg/ex-select-modal';

ExSelectModal.styles.card = {
  backgroundColor: '#1c1c1e',
  borderTopLeftRadius: 28,
  borderTopRightRadius: 28,
};
ExSelectModal.styles.title          = { fontSize: 17, fontWeight: '700', color: '#fff' };
ExSelectModal.styles.btnConfirm     = { backgroundColor: '#0a84ff' };
ExSelectModal.styles.btnConfirmText = { fontSize: 15, fontWeight: '600', color: '#fff' };
ExSelectModal.styles.backdropColor  = 'rgba(0,0,0,0.65)';

| Field | Type | Description | |-------|------|-------------| | card | ViewStyle | Main card (background, border radius, padding) | | pill | ViewStyle | Drag pill at the top | | title | TextStyle | Modal title (fontSize, color, fontFamily…) | | headerBorder | ViewStyle | Separator below the title | | btnConfirm | ViewStyle | Confirm button container | | btnCancel | ViewStyle | Cancel button container | | btnClear | ViewStyle | Clear button container | | btnConfirmText | TextStyle | Confirm button text | | btnCancelText | TextStyle | Cancel button text | | btnClearText | TextStyle | Clear button text | | backdropColor | string | Backdrop color + opacity (rgba(…)) |


Custom button example

<SelectModal
  btnConfirm={({ onPress }) => (
    <Pressable onPress={onPress} style={{ backgroundColor: '#0a84ff', borderRadius: 12, flex: 1, height: 48, alignItems: 'center', justifyContent: 'center' }}>
      <Text style={{ color: '#fff', fontWeight: '700' }}>Done</Text>
    </Pressable>
  )}
>
  <Select columns={[COL_UNIT]} value={draft} onChange={setDraft} />
</SelectModal>

Important rule

Place modals outside any ScrollView.

// ✅ Correct
return (
  <>
    <SafeAreaView>
      <ScrollView>...</ScrollView>
    </SafeAreaView>
    <SelectModal visible={open} onClose={onClose} onConfirm={onConfirm}>
      <Select columns={columns} value={draft} onChange={setDraft} />
    </SelectModal>
  </>
);

// ❌ Wrong — the modal scrolls with the content
return (
  <ScrollView>
    <SelectModal visible={open} .../>
  </ScrollView>
);