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

@nowtwo-llc/quick-switcher

v4.1.0

Published

Keyboard-driven command palette for the web, with nested drill-down searches, breadcrumb navigation, and usage-based result ranking. No runtime dependencies.

Readme

Quick Switcher

A keyboard-driven command palette for the web — the Cmd+K pattern from Slack, VS Code, and Spotlight, as a small library you can point at your own data.

  • No runtime dependencies. Ships as ESM, UMD, and a plain <script> global.
  • Nested searches. Any result can open a search of its own, with breadcrumbs.
  • Learns from use. Results are ranked by how often and how recently you have picked them, per search term.
  • Bring your own data. One callback in, results out — sync or async.

Live demo

Install

npm install @nowtwo-llc/quick-switcher

Quick start

import lstrQuickSwitcher from '@nowtwo-llc/quick-switcher';
import '@nowtwo-llc/quick-switcher/style.css';

const switcher = lstrQuickSwitcher({
  searchCallback(searchText, resultHandler) {
    const people = ['Zach', 'Stacy', 'Matt', 'Lightster', 'Baxter'];

    resultHandler.setResults(
      people.filter((person) => resultHandler.filters.isMatch(searchText, person))
    );
  },

  selectCallback(selected) {
    console.log('picked', selected.selectedValue);
  },
});

That's it — Cmd+K (macOS) or Ctrl+K now opens the switcher. Call switcher.open() to open it yourself.

Without a bundler

<link rel="stylesheet" href="/path/to/quick-switcher.min.css">
<script src="/path/to/quick-switcher.min.js"></script>
<script>
  var switcher = lstrQuickSwitcher({ /* ...options... */ });
</script>

Options

| Option | Type | Default | Description | | --- | --- | --- | --- | | searchCallback | function | no-op | Produces results for a query. See below. | | selectCallback | function | no-op | Runs when a result is chosen. Return false to keep the switcher open. | | selectChildSearchCallback | function | no-op | Runs when a nested search is entered. Return false to block it. | | searchDelay | number | 1000 | Debounce in milliseconds before searchCallback runs. Lower it for local data. | | hotKey | string \| null | 'K' | Key used with Cmd/Ctrl to open. null disables the hotkey entirely. | | trackChildrenAs | string | — | Tracker name. Set it to rank this search's results by usage. | | parentDom | HTMLElement | document.body | Element to mount into. |

The searchDelay default of 1000ms suits a search that hits the network. If you are filtering an in-memory list, set it to 0.

The search callback

searchCallback(searchText, resultHandler) {
  // ...
}

Call exactly one of:

  • resultHandler.setResults(items) — render items.
  • resultHandler.setError() — show the error pane.

Both are safe to call asynchronously. Results from a superseded query are discarded automatically, so a slow response cannot overwrite a newer one.

The handler also carries helpers:

  • resultHandler.filters.isMatch(needle, haystack) — case-insensitive substring or all-words match.
  • resultHandler.filters.areWordsFound(needle, haystack) — all-words match only.
  • resultHandler.sorters.tracker(name) — the tracker for name, for manual ranking via tracker.sort(items, searchText).

If your search is cancellable, return an abort function. The switcher calls it when the query changes before your results arrive:

searchCallback(searchText, resultHandler) {
  const controller = new AbortController();

  fetch(`/search?q=${encodeURIComponent(searchText)}`, {signal: controller.signal})
    .then((response) => response.json())
    .then((items) => resultHandler.setResults(items))
    .catch(() => resultHandler.setError());

  return () => controller.abort();
}

Results

A result is either a string, or an object:

| Property | Type | Description | | --- | --- | --- | | text | string \| function | Label, rendered as plain text. | | html | string \| function | Label, rendered as markup. Takes precedence over text. | | description | string \| object | Secondary line. Same text/html rules. | | searchCallback | function | Makes this result a nested search. | | breadcrumbText | string | Breadcrumb label for the nested search. | | trackerId | string | Identity for usage tracking. Required to be ranked. | | trackerStaticSort | number | Hard ordering applied before usage score. Lower comes first. | | searchDelay | number | Overrides the debounce inside a nested search. | | selectCallback | function | Overrides the select handler inside a nested search. | | selectChildSearchCallback | function | Overrides the child handler inside a nested search. | | trackChildrenAs | string | Tracker name for a nested search's own results. |

text is escaped; html is not. Never build an html value out of untrusted input.

Nested searches inherit any callback they don't declare from the search they were reached from, and Backspace on an empty box steps back out.

The selected result

selectCallback and selectChildSearchCallback receive:

| Member | Description | | --- | --- | | selectedValue | The result that was chosen. | | searchText | The query active at selection time. | | parent | The options object (or parent item) the result came from. | | domEvent | The originating DOM event. | | preventTracking() | Do not record this selection. | | preventSearchTextClearing() | Keep the query when entering a nested search. |

Usage-based ranking

Set trackChildrenAs and give results a trackerId:

lstrQuickSwitcher({
  trackChildrenAs: 'main',
  searchCallback(searchText, resultHandler) {
    resultHandler.setResults([
      {text: 'Dashboard', trackerId: 'dashboard'},
      {text: 'Settings', trackerId: 'settings'},
    ]);
  },
});

Selections are stored in localStorage under qswitcher-tracker-<name> and scored on the time-weighted model described in Slack's quick switcher write-up: recent picks are worth far more than old ones, and a pick for this search term outweighs general popularity. Storage failures degrade silently — a blocked or full localStorage disables ranking rather than breaking the switcher.

Instance API

| Method | Description | | --- | --- | | open() | Open, resetting to the root search. | | close() | Close. | | toggle() | Open if closed, close if open. | | isOpen() | Whether the switcher is currently open. | | destroy() | Detach all listeners and remove the element. Safe to call twice. |

Call destroy() when tearing down a view — the hotkey and navigation listeners live on document and are only removed here.

Styling

Import the compiled stylesheet, or the Sass source to theme it:

import '@nowtwo-llc/quick-switcher/style.css';
@use '@nowtwo-llc/quick-switcher/scss';

Every class is prefixed lstr-qswitcher-. The main hooks are -overlay, -container, -popup, -search, -results, -result-container, -result-description, -result-selected, -result-category, -breadcrumb, and -footer.

Theming

Every color reads through a CSS custom property, so you can retheme the switcher without overriding a single selector. Set the variables anywhere up the tree — :root, body, or a wrapper element:

:root {
  --lstr-qswitcher-bg:          #f5f6f7;  /* panel background */
  --lstr-qswitcher-fg:          #16181d;  /* primary text */
  --lstr-qswitcher-muted:       #5f6673;  /* descriptions, breadcrumbs */
  --lstr-qswitcher-surface:     #ffffff;  /* search box and footer */
  --lstr-qswitcher-accent:      #5468ff;  /* search focus ring and icon */
  --lstr-qswitcher-input-fg:    #000000;  /* search text */
  --lstr-qswitcher-selected-bg: #3274ce;  /* highlighted result */
  --lstr-qswitcher-selected-fg: #ffffff;  /* highlighted result text */
  --lstr-qswitcher-key-fg:      #969faf;  /* footer key hints */
  --lstr-qswitcher-close-fg:    #666666;  /* mobile close button */
  --lstr-qswitcher-overlay-bg:  rgba(0, 0, 0, 0.5);
}

Those are the defaults, so you only need to declare the ones you change. A dark theme is eleven lines:

@media (prefers-color-scheme: dark) {
  :root {
    --lstr-qswitcher-bg: #1c1f25;
    --lstr-qswitcher-fg: #e8eaed;
    --lstr-qswitcher-muted: #9aa1ac;
    --lstr-qswitcher-surface: #14161a;
    --lstr-qswitcher-accent: #6c9bff;
    --lstr-qswitcher-input-fg: #e8eaed;
    --lstr-qswitcher-selected-bg: #2f6fed;
    --lstr-qswitcher-key-fg: #8b93a1;
    --lstr-qswitcher-close-fg: #9aa1ac;
    --lstr-qswitcher-overlay-bg: rgba(0, 0, 0, 0.65);
  }
}

The switcher never inherits text color from the host page — it paints its own background, so inheriting would put light text on a light panel in any dark-themed app.

Browser support

Modern evergreen browsers. The build targets ES2020 and relies on AbortController, Element.closest, and AbortSignal-based listener removal.

Development

npm install
npm test          # vitest
npm run demo      # dev server for the demo page
npm run build     # dist/ JS, CSS, and type declarations
npm run lint

Upgrading from 3.x

See changelog.md for the full list. The changes most likely to affect you:

  • The package is now @nowtwo-llc/quick-switcher, published to npmjs.com. Installing no longer requires a registry mapping or an auth token.
  • text is now escaped. If you were passing markup in text, move it to html.
  • The library is now ESM-first; the UMD build and the lstrQuickSwitcher global are unchanged.
  • filters.isMatch is now case-insensitive.

License

MIT — see LICENSE.

Originally created by Matt Light (lightster) and adapted with thanks. Maintained by NowTwo LLC.