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

select-lab

v1.0.0

Published

Framework-agnostic super-select web components (single & multi) with server-side pagination, search, virtualization and data-driven theming.

Readme

select-lab

npm version license: MIT

Framework-agnostic super-select web components — one codebase, usable from Angular, Vue, Svelte and plain HTML/JS via the <select-lab> / <select-lab-multi> custom elements.

Built for the case a native <select> (and even select2 / ng-select) struggles with:

  • Continuous server-side pagination over 10k–15k+ rows (infinite scroll + row virtualization, so the DOM stays ~30 nodes).
  • Debounced field search with in-flight request cancellation and page caching.
  • Data-driven icons & colors per option (icon, icon color, text color, background — from your data, not hardcoded) — with adapters for emoji, raw SVG, any icon library that ships a custom element (<ion-icon>, <iconify-icon>, …), and ligature icon fonts. See Icons.
  • Display ≠ value: show name + description + icon, return just the id.
  • Closes on anything outside the flow — click outside, Escape, or scrolling the page/an ancestor — on by default. No detach, no manual scroll listeners to wire up yourself.
  • Real form control (ElementInternals, formAssociated) + ARIA combobox keyboard support.
  • Themeable via CSS custom properties and ::part() — no !important, plugs into whatever design system the host app already has.

Install

npm install select-lab lit @floating-ui/dom

lit and @floating-ui/dom are peer runtime deps (keeps the lib lean and dedupes across your app).

Quick start (vanilla)

<select-lab id="clients" value-field="id" label-field="name"
  description-field="email" icon-field="icon" icon-color-field="color"
  placeholder="Search client…"></select-lab>

<script type="module">
  import 'select-lab/single'; // registers <select-lab>

  const el = document.getElementById('clients');
  el.dataSource = async ({ search, offset, limit, signal }) => {
    const r = await fetch(`/api/clients?search=${search}&skip=${offset}&limit=${limit}`, { signal });
    return { items: await r.json() };      // bare array also accepted; `total` optional
  };
  el.addEventListener('change', (e) => console.log(e.detail.value)); // the id
</script>

Pure-HTML alternative (library does the fetch):

<select-lab src="/api/clients" search-param="search" offset-param="skip"
  limit-param="limit" value-field="id" label-field="name"></select-lab>

Entry points

| Import | Registers | Value | | --- | --- | --- | | select-lab/single | <select-lab> | scalar (the chosen value-field) | | select-lab/multi | <select-lab-multi> | array of value-fields (renders chips) | | select-lab | both | — | | select-lab/core | — | SelectLabBase + types (build your own) |

Data contract

type LoadParams  = { search: string; offset: number; limit: number; signal: AbortSignal };
type DataSource  = (p: LoadParams) => Promise<{ items: T[]; total?: number } | T[]>;
  • Return { items, total } for an exact end-of-list; or a bare T[] (compatible with services that already return arrays) — end-of-list is then inferred from a short page.
  • .items = [...] (a static array property) turns it into a normal in-memory select (client-side filter, no pagination).

Icons

Works with whatever icon system your app already has — no rewrite needed. icon-field auto-detects the shape of the value; for full control, .iconRenderer calls straight into your own icon library's API.

| Your icon data looks like… | How to wire it | | --- | --- | | Emoji / plain glyph ("🧑‍💼") | Nothing to configure — just icon-field. | | An image URL / data: URI | Auto-detected, rendered as <img>. | | Inline SVG markup ("<svg…>…</svg>") | Auto-detected, injected as real DOM (not escaped text). | | A tag from a custom-element icon library ("<ion-icon name='home'>", <iconify-icon>, …) | Same auto-detection — if that library registered its element globally, it upgrades inside the shadow root for free (the custom element registry isn't scoped per shadow tree). | | A ligature icon font (Material Symbols/Icons, …) | icon-font-family="Material Symbols Rounded" — no CSS to write. Unlike a plain CSS class, this correctly reaches through the Shadow DOM. | | A class-based icon font (Font Awesome, Bootstrap Icons, …) | inherit-styles clones the page's stylesheets into the shadow root, or resolve the glyph yourself upstream. | | Anything your own JS API can render | .iconRenderer = (item, iconValue) => … — return a Lit TemplateResult or a markup string; full control, any icon library. |

Framework components (Angular <mat-icon>, a Vue/Svelte icon component) can't be injected this way — they need their framework's own compiler attached to the node. Resolve them to one of the forms above upstream (most icon libraries already expose a raw-SVG or unicode form for exactly this reason).

Key attributes / properties

| Attr / prop | Default | Purpose | | --- | --- | --- | | value-field | id | field returned as the value | | label-field | label | primary text | | description-field | — | optional second line | | icon-field / icon-color-field | — | per-item icon + its color | | text-color-field / bg-field | — | per-item text color / row background | | icon-type | auto | auto | image | font | emoji | | icon-font-family | — | ligature icon font by family name (see Icons) | | icon-font-class | — | extra class on the icon glyph, for use with inherit-styles | | inherit-styles | false | clone the page's stylesheets into the shadow root (class-based icon fonts) | | page-size | 50 | rows per request | | debounce | 300 | search debounce (ms) | | option-height | 44 | fixed row height (px) — used by virtualization | | close-on-scroll | true | close when the page/an ancestor scrolls outside the select; set false to have the panel follow the control instead | | .dataSource / src / .items | — | data (see Data contract) | | .renderOption / .renderLabel | — | full custom row/label templates (power users) | | .iconRenderer(item, iconValue) | — | full custom icon rendering (power users, any icon library's own API) | | .resolveValue(value) | — | hydrate a preselected value not in the buffer |

Events: change ({ value, item } or { value, items }), search, open, close.

Theming

Override CSS custom properties, or target internals with ::part():

select-lab { --sl-accent: #10b981; --sl-radius: 12px; }
select-lab::part(panel)  { border-radius: 16px; }
select-lab::part(option) { padding-inline: 16px; }

Dark mode follows prefers-color-scheme; force it with theme="dark" / theme="light".

Framework usage

Angular — allow the custom element, bind the .dataSource property and the change event:

@NgModule({ schemas: [CUSTOM_ELEMENTS_SCHEMA] }) // or in a standalone component
<select-lab [dataSource]="loadClients" value-field="id" label-field="name"
  (change)="onChange($event)"></select-lab>
loadClients = ({ search, offset, limit }) =>
  firstValueFrom(this.clientService.getClients(offset, limit, search)); // already returns Client[]

A ControlValueAccessor adapter (for reactive forms / ngModel) is planned as select-lab/angular.

Vue — treat it as a custom element, then v-model-style bind:

// vite: app.config.compilerOptions.isCustomElement = t => t.startsWith('select-lab')
<select-lab :value-field="'id'" label-field="name"
  :.dataSource="loadClients" @change="v = $event.detail.value" />

Svelte — works natively:

<select-lab label-field="name" bind:this={el} on:change={(e) => (v = e.detail.value)} />
<script>onMount(() => (el.dataSource = loadClients));</script>

Develop & test

npm run dev            # demo harness at http://localhost:5173 (10k-row mock API)
npm run build          # tree-shakeable ESM library → dist/
npm run typecheck
npm test               # DataController engine (pagination/search/cache/abort)
npm run test:browser   # drives the real UI in Chrome (needs the dev server running)

Contributing

Bug reports, fixes and ideas are welcome — see CONTRIBUTING.md for how to get set up and what to run before opening a PR.

License

MIT © MonsDz — see LICENSE.

Security

See SECURITY.md for how to report a vulnerability.

Links