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

@sam-mu/react-dropdown

v0.1.1

Published

Accessible, themeable React Dropdown / Select with multi-select, search, async loading, virtualization, creatable options, and color variants — built on Downshift + Floating UI.

Readme

@sam-mu/react-dropdown

A reusable, controlled, headless-friendly Dropdown / Select for React — written in JavaScript (JSX), no TypeScript build step. Single-select works out of the box; multi, searchable, clearable, creatable are opt-in flags that default OFF.

Built on Downshift (a11y/interaction) + Floating UI (positioning). Ships unstyled-able with CSS-variable theming. Peer-depends on React only.

Placeholder package name — rename before publishing.

Install

npm install @sam-mu/react-dropdown
# peer deps
npm install react react-dom

Usage

import { Dropdown } from '@sam-mu/react-dropdown';
import '@sam-mu/react-dropdown/styles.css'; // or roll your own via classNames

function Example() {
  const [value, setValue] = React.useState(null);
  return (
    <Dropdown
      label="Fruit"
      options={['Apple', 'Banana', 'Cherry']}
      value={value}
      onChange={setValue}
      clearable
    />
  );
}

Multi + searchable

<Dropdown
  multi
  searchable
  clearable
  options={[
    { value: 'us', label: 'United States', group: 'Americas' },
    { value: 'br', label: 'Brazil', group: 'Americas' },
    { value: 'de', label: 'Germany', group: 'Europe' },
  ]}
  value={values}
  onChange={setValues}
  maxTagsVisible={2}
/>

Async

<Dropdown
  searchable
  isLoading={loading}
  onSearch={(q) => fetchOptions(q)}   // debounced (searchDebounceMs, default 250)
  options={results}
/>

Phone / country-code wrapper

import { PhoneField } from '@sam-mu/react-dropdown';

<PhoneField
  countryCode={code}
  phone={phone}
  onChange={({ countryCode, phone }) => { /* ... */ }}
/>

Props

See the implementation spec §5 for the full prop table. Highlights:

| Prop | Type | Default | Notes | |---|---|---|---| | options | array | object | string | string[] | — | Normalized internally | | value / defaultValue | V | V[] | null | — | Array when multi | | onChange | (value, meta) => void | — | meta.action, meta.option | | multi searchable clearable creatable | boolean | false | Opt-in | | onSearch / searchDebounceMs | fn / number | 250 | Server-side filtering | | renderOption renderValue renderEmpty | fn | — | Render slots | | unstyled / classNames / size | — | — | Theming |

Value contract

When multi is true, value/onChange deal in arrays; otherwise a single value or null. Normalized internally — the caller never branches on count.

Advanced features

Type-ahead — in non-searchable mode, start typing letters to jump the highlight to the first matching option (like a native <select>).

Select all / count (multi) — pass showSelectAll to add a Select all / Clear all toggle and an "N selected" counter to the menu header.

<Dropdown multi showSelectAll maxSelected={5} options={opts} value={v} onChange={setV} />

Backspace removes the last chip in multi mode when the query is empty.

Match highlighting — search matches are wrapped in <mark class="dd-mark">.

Icon + description per option — the default row renders option.icon and option.description:

options={[{ value: 'us', label: 'United States', icon: '🇺🇸', description: '+1' }]}

Virtualization — long lists auto-virtualize past virtualizeThreshold (default 60) via @tanstack/react-virtual. Force with virtualize={true} / disable with false. (Flat lists only; grouped lists render normally.)

Async pagination — infinite scroll:

<Dropdown searchable isLoading={loading} onSearch={load}
  options={rows} hasMore={hasMore} onLoadMore={loadNextPage} />

Creatable with persistenceonCreate may return an option or a Promise of one:

<Dropdown creatable multi options={tags} value={v} onChange={setV}
  onCreate={async (label) => { const opt = await api.createTag(label); return opt; }} />

Custom data shapesgetOptionValue / getOptionLabel (or normalizeOption) map arbitrary objects without pre-transforming your data.

Imperative handleref exposes { open, close, toggle, clear, focus, getValue }:

const ref = useRef(null);
<Dropdown ref={ref} options={opts} />;
ref.current.open();

Form integration

A hidden <input name> is rendered for plain HTML form posts. With react-hook-form:

import { Controller, useForm } from 'react-hook-form';
import { Dropdown } from '@sam-mu/react-dropdown';

function Form() {
  const { control, handleSubmit } = useForm({ defaultValues: { country: null } });
  return (
    <form onSubmit={handleSubmit(console.log)}>
      <Controller
        name="country"
        control={control}
        rules={{ required: true }}
        render={({ field, fieldState }) => (
          <Dropdown
            label="Country"
            options={countries}
            value={field.value}
            onChange={field.onChange}
            onOpenChange={(o) => !o && field.onBlur()}
            error={fieldState.error && 'Required'}
          />
        )}
      />
      <button type="submit">Save</button>
    </form>
  );
}

Formik is analogous: wire value={field.value} and onChange={(v) => setFieldValue(name, v)}.

Accessibility

WAI-ARIA combobox/listbox semantics (via Downshift): role="combobox" with aria-expanded / aria-controls, role="listbox"/option, aria-activedescendant tracking, full keyboard support (↑/↓, Enter, Esc, Home/End, type-ahead), and a labelled trigger. jest-axe runs against the closed and open (multi + search) states in CI with zero violations.

Theming

Color variants

Pass variant for a semantic accent color — it tints the focus ring, selected rows, tags/chips, checkmarks, and search highlight, while the trigger/menu surfaces stay neutral:

<Dropdown variant="success" options={opts} />   // green
<Dropdown variant="danger"  options={opts} />   // red
<Dropdown variant="warning" options={opts} />   // amber
// primary (default) | success | danger | warning | info | neutral

Every accent is derived from a single --dd-accent via color-mix, so a custom color is just one variable — no per-token overrides:

.my-brand { --dd-accent: #7c3aed; }   /* violet, everywhere */
<Dropdown className="my-brand" options={opts} />

Tokens & dark mode

Override any --dd-* CSS variable (defined on :root so the portaled menu inherits them), pass classNames, or set unstyled to drop the default CSS entirely. Dark mode is opt-in: add data-theme="dark" (or class="dark") to any ancestor — accents adapt automatically since they blend toward --dd-fg.

Key tokens: --dd-accent (drives all accent colors), --dd-bg, --dd-fg, --dd-border, --dd-radius, --dd-menu-bg, --dd-menu-shadow, --dd-option-hover, --dd-danger, --dd-size-{sm,md,lg}.

Scripts

npm run dev     # Vite playground at http://localhost:5173
npm run build   # tsup -> dist (ESM + CJS + bundled CSS)
npm test        # vitest (unit + component + jest-axe)
npm run size    # size-limit bundle budget

Status

Implements Tier 0 through Tier 2 and much of Tier 3 from the spec: single/multi/ search/clear/creatable, grouped + disabled options, icon/description rows, render slots, match highlighting, type-ahead, select-all + count, async search + infinite scroll, virtualization, imperative ref, hidden form inputs + RHF recipe, hand-written .d.ts types, jest-axe gating, size-limit budget, opt-in dark mode, and the PhoneField wrapper. Storybook + Changesets/CI remain as follow-ups.