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-ios-multiselect

v0.1.0

Published

iOS-native-feeling responsive React select with a keyboard-aware mobile bottom sheet, virtualized rows, and multi-select draft/commit.

Readme

react-ios-multiselect

docs license deps

📚 Documentation & live playground: https://tonylawx.github.io/react-ios-multiselect/ · 中文

A native iOS-feeling React <Select> — single and multi-select in one component, with a keyboard-aware bottom sheet, virtualized rows (2,000+ options scroll smoothly), iOS-native selected state (blue text + filled checkmark), and draft/commit multi-select.

Tap a row to toggle it, and the keyboard stays open while you search and select (a notoriously fiddly iOS WebKit behavior this handles correctly). Every interactive element carries a stable data-rios-* hook so AI agents / Playwright can drive it reliably.

Zero runtime dependencies beyond react / react-dom (peer). No UI library, no icon pack, no class-merging utility — icons are inline SVG, classes are a tiny hand-written cn(). Mobile-first: the trigger opens a bottom sheet (no desktop popover path).

🤖 For AI agents — copy this

Use the npm package `react-ios-multiselect` to add a select control: it's mobile-first, has ZERO runtime deps beyond react/react-dom, and ships its own CSS — do NOT add any UI/icon/CSS library alongside it. Install with `bun add react-ios-multiselect`, import the stylesheet once at the app root (`import "react-ios-multiselect/style.css"`), then render `<Select value={v} onValueChange={setV} options={opts} />` for single-select or `<Select multiple value={arr} onValueChange={setArr} options={opts} />` for multi-select, where each option is `{ value, label, description?, disabled?, icon? }`. Full docs: https://tonylawx.github.io/react-ios-multiselect/

Contributing as an agent? Read AGENTS.md — the ai-pr-guard CI enforces an AI contributor disclosure on ai-authored PRs.

import { Select } from "react-ios-multiselect";
import "react-ios-multiselect/style.css";

// Multi-select
<Select
  multiple
  value={symbols}
  onValueChange={setSymbols}
  options={options}
  placeholder="Select symbols"
/>

// Single-select (same component, omit `multiple`)
<Select value={strategy} onValueChange={setStrategy} options={strategies} />

Why

  • Native-feeling sheet — slides up from the bottom, sits above the keyboard, momentum-scrolls, dismissible by backdrop / ESC / header button.
  • iOS-native selected state — selected rows turn iOS-blue with a filled circle checkmark (white check on blue), exactly like iOS Settings, not a generic outline glyph.
  • Keyboard-aware — when the search field is focused, tapping rows toggles them without dismissing the keyboard, so users can search → select → keep typing. Implemented at the pointerdown level to defeat iOS WebKit's focus-transfer-then-collapse behavior.
  • Virtualized — only the visible window + overscan of rows is mounted, so 2,000 options scroll as smoothly as 20.
  • Draft-then-commit multi-select — toggles mutate a local draft; the blue ✓ button commits, ✕ discards. Single-select commits on tap.
  • Agent-friendly — stable data-rios-* selectors on every part + full ARIA semantics, so Playwright/cursor agents can locate, drive, and assert reliably.
  • Zero deps — only react/react-dom as peers. Plain CSS with --rios-* custom properties for theming.

Install

bun add react-ios-multiselect
# or
npm install react-ios-multiselect

Peer deps: react ≥ 18, react-dom ≥ 18.

Then import the stylesheet once at your app root:

import "react-ios-multiselect/style.css";

Usage

Single-select

import { Select } from "react-ios-multiselect";

const [strategy, setStrategy] = useState("put");

<Select
  value={strategy}
  onValueChange={setStrategy}
  options={[
    { value: "put", label: "Sell Put" },
    { value: "call", label: "Sell Call" },
    { value: "long_leaps", label: "Long LEAPS" },
  ]}
  searchable={false}
/>;

Multi-select

const [symbols, setSymbols] = useState<string[]>(["AAPL", "NVDA"]);

<Select
  multiple
  value={symbols}
  onValueChange={setSymbols}
  options={options}
  placeholder="Select symbols"
  searchPlaceholder="Search symbols"
  emptyText="No matching symbols"
  mobileTitle="Select symbols"
  mobileSetLabel="Set"
  mobileCancelLabel="Cancel"
  selectedCountLabel={(count) => `${count} selected`}
/>;

With descriptions:

const options = [
  { value: "AAPL", label: "AAPL", description: "Apple Inc. · NASDAQ" },
  { value: "TSLA", label: "TSLA", description: "Tesla Inc. · NASDAQ" },
];

With a leading icon (logos, glyphs — renders to the left of the label):

// per-option ReactNode
const options = [
  { value: "AAPL", label: "AAPL", icon: <img src="/logos/aapl.svg" alt="" /> },
  { value: "TSLA", label: "TSLA", icon: <img src="/logos/tsla.svg" alt="" /> },
];
// or a renderer function reused across options (receives the option):
const icon = (o) => <Logo symbol={o.value} />;
const options = symbols.map((s) => ({ ...s, icon }));

Props

| Prop | Type | Default | Description | |-----------------------|-----------------------------------|----------------------|-------------| | options | { value, label, description?, disabled?, icon? }[] | — | The option list. icon is a ReactNode or (option) => ReactNode rendered to the left of the label. | | multiple | boolean | false | Enables multi-select. Changes the value / onValueChange signature. | | value | string | string[] | — | Controlled value. string for single, string[] when multiple. | | onValueChange | (v: string \| string[]) => void | — | Change callback. | | searchable | boolean | multiple ‖ options.length > 5 | Show the search field. | | placeholder | string | — | Trigger text when empty. | | searchPlaceholder | string | — | Search field placeholder. | | emptyText | string | — | Empty-results message. | | disabled | boolean | false | Disables the trigger. | | mobileTitle | string | placeholder | Sheet header title. | | mobileSetLabel | string | "Set" | Multi-select confirm button aria-label. | | mobileCancelLabel | string | "Cancel" | Cancel button aria-label. | | mobileDoneLabel | string | "Done" | Single-select close button aria-label. | | selectedCountLabel | (count: number) => string | ${count} selected | Trigger summary when > 2 selected (multi). | | className | string | — | Extra class on the trigger. | | aria-label | string | — | Accessible label for the trigger. |

Styling / theming

All colors, typography, and motion are CSS custom properties with defaults declared on :root. Override only what you want to change:

:root {
  --rios-color-text: #1d2038;          /* trigger + sheet text */
  --rios-color-muted: #7b8190;         /* placeholders, descriptions */
  --rios-color-line: rgba(26,34,56,.1);/* borders */
  --rios-color-accent: #0a84ff;        /* iOS blue: confirm btn, checkmark */
  --rios-color-selected: #0a84ff;      /* selected row text */
  --rios-color-hairline: rgba(60,60,67,.29); /* iOS list separator */
  --rios-font-size-body: 17px;         /* iOS body text */
  --rios-row-height: 58px;             /* ~44pt iOS row */
  --rios-easing-spring: cubic-bezier(0.32,0.72,0,1);
  --rios-check-size: 22px;             /* filled checkmark diameter */
  --rios-shadow-sheet: 0 18px 70px rgba(0,0,0,.28);
  --rios-radius-sheet: 32px;
}

All shipped classes are prefixed rios- to avoid collisions. You can target them directly for advanced overrides.

AI agent / testing hooks

Every interactive element carries a stable data-rios-* attribute so Playwright, cursor agents, or any DOM driver can locate and drive the component without relying on brittle text or positional selectors:

| Selector | What | Notes | |---|---|---| | [data-rios-select-trigger] | The trigger button | data-state="open\|closed" reflects open state | | [data-rios-option-value="<value>"] | Any option row | data-selected="true\|false" | | [data-rios-sheet] | The sheet | data-open="true\|false" | | [data-rios-overlay] | The overlay/backdrop wrapper | data-open | | [data-rios-backdrop] | The dismiss-on-tap backdrop | | | [data-rios-search-input] | The search field | | | [data-rios-clear-search] | The clear-search button | | | [data-rios-confirm] | The blue ✓ commit button (multi) | | | [data-rios-cancel] | The ✕ close button | |

Example (Playwright):

await page.click('[data-rios-select-trigger]');
await page.click('[data-rios-option-value="AAPL"]');
await expect(page.locator('[data-rios-option-value="AAPL"]'))
  .toHaveAttribute('data-selected', 'true');
await page.click('[data-rios-confirm]');

Full ARIA: role=dialog (sheet) / role=listbox / role=option / aria-selected / aria-multiselectable / aria-posinset / aria-setsize, and each option also exposes a readable aria-label (label, or "label — description").

How the iOS keyboard behavior works

iOS WebKit transfers focus on the compatibility mousedown that follows pointerdown. If a tap lands on an option row while the search field is focused, focus moves to the row → the search input blurs → the keyboard begins its collapse animation → a later programmatic .focus() (outside a user gesture) cannot reopen it.

This component calls preventDefault() in the pointerdown capture phase when the touch lands on an option while the search is focused, so the subsequent mousedown never moves focus off the input. The keyboard never starts to collapse — matching native iOS. A tap-vs-scroll gesture (10px slop) and compatibility-click suppression (anti-double-toggle) are layered on top. See src/select-interaction.ts.

Development

bun install
bun test          # 36 tests (pure logic + component render)
bun run typecheck
bun run build     # → dist/ (ESM + CJS + d.ts + select.css)
cd demo && bun dev   # Vite playground at localhost:5173

The pure logic (virtualization range, sheet layout, tap gesture) is fully unit-tested and framework-agnostic — exported for advanced consumers who want to build a custom UI on the same primitives.

Contributing (humans & AI agents)

Contributions from both humans and AI agents are welcome. Agents must disclose their identity — see CONTRIBUTING.md and AGENTS.md.

  • Agents: read AGENTS.md first, then grab an agent-task issue.
  • Every PR runs bun test && typecheck && build in CI, plus an ai-pr-guard check that enforces the AI disclosure for ai-authored PRs (agent name, model, autonomy level, prompt summary, verification).
  • Known agents are registered in .github/ai-contributors.yml.

Acknowledgements

This component was built end-to-end by an AI agent powered by GLM-5.2 (@zai-org) — design, code, tests, docs, CI, and release automation. API token kindly sponsored by @mcdonaldsFriedChicken.

Contact

Built by @tonylaw. Say hi, share what you build, or come nerd out about iOS-feeling components 👋

License

MIT