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

@automattic/commands

v0.4.2

Published

Command palette components powered by cmdk

Readme

@automattic/commands

Beta Notice

This package is in beta. APIs, behavior, and styling may change without notice until a stable release is announced.


Config-driven CMD+K command palette for React, powered by cmdk.

Pass a flat Command[] array and get a themed palette with fuzzy search, route variable resolution, recently-used tracking, and zero CSS import.

Install

npm install @automattic/commands

Peer dependencies: react and react-dom >= 18.

Quick start

import { Commands } from '@automattic/commands';
import type { Command } from '@automattic/commands';

const commands: Command[] = [
	{
		id: 'dashboard',
		title: 'Dashboard',
		description: 'Go to dashboard',
		route: '/dashboard',
		group: 'Pages',
		keywords: [ 'home', 'overview' ],
	},
	{
		id: 'toggle-theme',
		title: 'Toggle Dark Mode',
		action: () => {
			document.documentElement.classList.toggle( 'is-dark' );
		},
		group: 'Actions',
		shortcut: 'D',
	},
];

function App() {
	return <Commands commands={ commands } onNavigate={ path => router.push( path ) } />;
}

Press Cmd+K (macOS) or Ctrl+K (Windows/Linux) to open. The palette renders a dialog overlay, search input, and command list — no extra CSS import needed.

API reference

Command

Each entry in the commands array describes one palette item.

| Field | Type | Required | Description | | ------------- | ------------ | -------- | --------------------------------------------------------------------------------------------------------------------------- | | id | string | Yes | Unique identifier, also used for recency tracking. | | title | string | Yes | Display title and primary search target. | | description | string | No | Secondary line shown below the title. | | route | string | No | Navigation path. Supports :param variables (see Resolver pattern). Mutually exclusive with action. | | action | () => void | No | Callback for non-navigation commands. Mutually exclusive with route. | | group | string | No | Group label for visual sections (e.g. "Pages", "Actions"). | | keywords | string[] | No | Extra search terms not displayed in the UI. | | icon | ReactNode | No | Icon element rendered before the title. | | shortcut | string | No | Keyboard shortcut hint shown on the right (e.g. "⌘L"). Display-only — does not register a listener. |

Every command must have either route or action (but not both). In development, invalid commands produce console.warn messages.

CommandsProps

Props for the <Commands /> component.

| Prop | Type | Default | Description | | ------------------ | ------------------------------------------------------------------------------------------------ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | commands | Command[] | — | Array of command definitions. | | resolver | (param: string, selections: Record<string, string>) => ResolvedParam \| Promise<ResolvedParam> | — | Resolves route :param variables one at a time. See Resolver pattern. | | onNavigate | (path: string) => void | — | Called with the fully resolved path when a route command is selected. | | triggerKey | string | "Mod+k" | Keyboard shortcut to toggle the palette. Mod maps to Cmd on macOS, Ctrl elsewhere. Modifiers are +-separated: "Meta+k", "Ctrl+Shift+p". | | placeholder | string | "Search commands..." | Placeholder text for the search input. | | filter | (value: string, search: string) => number | cmdk built-in | Custom scoring function. Return 0 to hide, 1 to rank highest. | | emptyState | ReactNode | "No results found." | Content shown when no commands match the search. | | showRecent | boolean | true | Show recently selected commands when the search input is empty. | | recentLimit | number | 5 | Maximum number of recent commands to display. | | recentStorageKey | string | "@automattic/commands:recent" | localStorage key for persisting recent commands. |

Utility exports

The package also exports route-resolution helpers:

import { resolveRoute, extractParams, replaceRouteParam } from '@automattic/commands';

| Function | Signature | Description | | ------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | | extractParams | (route: string) => string[] | Extracts :param names from a route. "/apps/:id/logs"["id"]. | | replaceRouteParam | (route: string, name: string, value: string) => string | Replaces a single named parameter in a route string. | | resolveRoute | (route: string, resolver?, selections?) => Promise<ResolveRouteResult> | Runs the full resolution pipeline: extract params → call resolver per param → return resolved path, unresolved params, and selections. |

Types

import type {
	Command,
	CommandsProps,
	ResolvedParam,
	ResolveRouteResult,
	UnresolvedParam,
} from '@automattic/commands';

ResolvedParamstring | string[]. A string means the param is fully resolved; an array means the palette shows a sub-layer for the user to pick one.

ResolveRouteResult{ path: string; unresolved: UnresolvedParam[]; selections: Record<string, string> }. The path with resolved params replaced, unresolved params listed with optional selectable options, and accumulated param selections.

UnresolvedParam{ name: string; options?: string[] }. A param that still needs a value, optionally with options for the user to choose from.

Resolver pattern

Routes can contain :param placeholders that are resolved at runtime via the resolver prop. This lets you inject dynamic context (current app ID, environment, user) without baking it into command definitions.

How it works

  1. User selects a command with a parameterized route (e.g. /apps/:appId/:env/logs).
  2. The palette extracts params from left to right and calls resolver(param, selections) for the next unresolved param.
  3. selections contains values from earlier params, including auto-resolved strings and user-selected options.
  4. For each param, the resolver returns:
    • A string → the param is replaced in the path immediately, added to selections, and the next param is resolved.
    • A string array → the palette shows a sub-layer where the user picks one option. Remaining params are resolved after the user selects a value.
    • Anything else → the param is listed as unresolved with no options.
  5. Once all params are resolved, onNavigate fires with the final path.

Example: dependent params

import type { CommandsProps } from '@automattic/commands';

const resolver: CommandsProps[ 'resolver' ] = async ( param, selections ) => {
	if ( param === 'appId' ) {
		return [ 'my-app', 'other-app' ];
	}

	if ( param === 'env' ) {
		const envs = await fetchEnvironments( selections.appId );
		return envs.map( env => env.name );
	}

	return [];
};

<Commands
	commands={ [ { id: 'logs', title: 'App logs', route: '/apps/:appId/:env/logs' } ] }
	resolver={ resolver }
	onNavigate={ path => router.push( path ) }
/>;

When the user selects "App logs":

  1. The palette asks the resolver for appId options and shows a sub-layer.
  2. The user picks an app, and that value is added to selections.
  3. The palette asks the resolver for env options with selections.appId available.
  4. onNavigate fires with /apps/my-app/production/logs.

Resolver tips

  • The resolver can be sync or async. Async resolvers trigger a loading state in the palette.
  • If the resolver throws, the error is logged and shown in the palette — no navigation occurs.
  • Multiple params are resolved sequentially, one resolver() call per param.
  • Press Backspace on an empty input during param selection to cancel and return to the command list. Press Backspace from the error state to dismiss it.

Theming

The default theme is bundled with <Commands /> — no CSS import needed. Override any value by setting --cmdk-* custom properties on :root or any ancestor of the palette.

CSS custom properties

Surface

| Variable | Default | Description | | ------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------- | | --cmdk-bg | #fff | Dialog background. | | --cmdk-text | #1e1e1e | Primary text color. | | --cmdk-border | #dcdcde | Border color (dialog and input divider). | | --cmdk-radius | 4px | Dialog border radius. | | --cmdk-shadow | 0 16px 40px rgba(0,0,0,0.12) | Dialog box shadow. | | --cmdk-font | -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif | Font stack. | | --cmdk-max-height | 360px | Max height for the scrollable command list. |

Overlay and dialog

| Variable | Default | Description | | ---------------------------- | ------------------ | -------------------------------------- | | --cmdk-overlay-bg | rgba(0,0,0,0.45) | Overlay backdrop color. | | --cmdk-overlay-z | 999 | Overlay z-index. | | --cmdk-dialog-top | 14vh | Vertical position from top. | | --cmdk-dialog-width | 640px | Max dialog width. | | --cmdk-dialog-margin | 32px | Viewport safety margin. | | --cmdk-dialog-border-width | 1px | Dialog and input divider border width. | | --cmdk-dialog-z | 1000 | Dialog z-index. |

Search input

| Variable | Default | Description | | --------------------------- | ---------------------------------- | ------------------------------------------------ | | --cmdk-input-height | 52px | Minimum height of the input row. | | --cmdk-input-padding-x | 16px | Horizontal padding of the input row. | | --cmdk-input-bg | #fff | Input background. | | --cmdk-input-text | #1e1e1e | Input text color. | | --cmdk-input-font-size | 16px | Input font size. | | --cmdk-input-line-height | 24px | Input line height. | | --cmdk-input-icon-size | 18px | Search icon size. | | --cmdk-input-icon-gap | 12px | Gap between search icon and input. | | --cmdk-placeholder | #757575 | Placeholder and empty/loading text color. | | --cmdk-focus | #3858e9 | Focus accent (input border, selected indicator). | | --cmdk-input-focus-shadow | inset 0 -1px 0 var(--cmdk-focus) | Box shadow when input is focused. |

List

| Variable | Default | Description | | --------------------- | ------- | ---------------------------------------- | | --cmdk-list-padding | 4px | Padding inside the scrollable list area. |

Group headings

| Variable | Default | Description | | ---------------------------------- | --------- | --------------------------- | | --cmdk-group-heading | #646970 | Heading text color. | | --cmdk-group-heading-font-size | 12px | Heading font size. | | --cmdk-group-heading-line-height | 16px | Heading line height. | | --cmdk-group-heading-padding-y | 8px | Heading vertical padding. | | --cmdk-group-heading-padding-x | 12px | Heading horizontal padding. | | --cmdk-group-heading-font-weight | 500 | Heading font weight. |

Items

| Variable | Default | Description | | -------------------------------- | --------- | ---------------------------------------- | | --cmdk-item-padding-y | 10px | Item vertical padding. | | --cmdk-item-padding-x | 12px | Item horizontal padding. | | --cmdk-item-radius | 4px | Item border radius. | | --cmdk-item-font-size | 14px | Item font size. | | --cmdk-item-line-height | 20px | Item line height. | | --cmdk-item-gap | 12px | Gap between icon, content, and shortcut. | | --cmdk-item-title-font-weight | 500 | Title font weight. | | --cmdk-item-selected-bg | #f6f7f7 | Selected item background. | | --cmdk-item-selected-text | #1e1e1e | Selected item text color. | | --cmdk-item-selected-indicator | #3858e9 | Left border accent on selected item. | | --cmdk-transition-duration | 100ms | Hover/selection transition duration. | | --cmdk-disabled-opacity | 0.5 | Opacity for disabled items. |

Icons

| Variable | Default | Description | | ------------------- | --------- | ---------------------- | | --cmdk-icon-size | 20px | Icon width and height. | | --cmdk-icon-color | #646970 | Icon color. |

Description

| Variable | Default | Description | | ------------------------------------- | --------- | ---------------------------------- | | --cmdk-description | #646970 | Description text color. | | --cmdk-item-description-font-size | 12px | Description font size. | | --cmdk-item-description-line-height | 16px | Description line height. | | --cmdk-item-description-gap | 2px | Gap between title and description. |

Shortcut badge and type label

| Variable | Default | Description | | ------------------------------ | -------------------------- | ---------------------------------------- | | --cmdk-shortcut | #646970 | Shortcut text color (fallback). | | --cmdk-shortcut-text | inherits --cmdk-shortcut | Shortcut text color. | | --cmdk-shortcut-bg | #f6f7f7 | Shortcut badge background. | | --cmdk-shortcut-border | #dcdcde | Shortcut badge border color. | | --cmdk-shortcut-border-width | 1px | Shortcut badge border width. | | --cmdk-shortcut-radius | 4px | Shortcut badge border radius. | | --cmdk-shortcut-padding-y | 2px | Shortcut badge vertical padding. | | --cmdk-shortcut-padding-x | 6px | Shortcut badge horizontal padding. | | --cmdk-type-label | #646970 | Type label color ("Link" / "Action"). | | --cmdk-item-meta-font-size | 12px | Font size for shortcut and type label. | | --cmdk-item-meta-line-height | 16px | Line height for shortcut and type label. |

Empty and loading states

| Variable | Default | Description | | ---------------------------- | ------- | --------------------------------- | | --cmdk-empty-padding-y | 32px | Empty state vertical padding. | | --cmdk-empty-padding-x | 16px | Empty state horizontal padding. | | --cmdk-empty-font-size | 14px | Empty state font size. | | --cmdk-empty-line-height | 20px | Empty state line height. | | --cmdk-loading-padding-y | 24px | Loading state vertical padding. | | --cmdk-loading-padding-x | 16px | Loading state horizontal padding. | | --cmdk-loading-font-size | 14px | Loading state font size. | | --cmdk-loading-line-height | 20px | Loading state line height. |

Dark mode example

.dark-theme {
	--cmdk-bg: #1e1e1e;
	--cmdk-text: #f0f0f0;
	--cmdk-border: #3c3c3c;
	--cmdk-shadow: 0 16px 40px rgba( 0, 0, 0, 0.4 );
	--cmdk-overlay-bg: rgba( 0, 0, 0, 0.7 );
	--cmdk-input-bg: #1e1e1e;
	--cmdk-input-text: #f0f0f0;
	--cmdk-placeholder: #a0a0a0;
	--cmdk-item-selected-bg: #2c2c2c;
	--cmdk-item-selected-text: #f0f0f0;
	--cmdk-group-heading: #a0a0a0;
	--cmdk-description: #a0a0a0;
	--cmdk-shortcut-bg: #2c2c2c;
	--cmdk-shortcut-border: #3c3c3c;
}

WPDS integration

The theme is WPDS-aware: when WordPress Design System CSS variables (e.g. --wpds-color-bg-surface-neutral) are present, the palette uses them automatically. No WPDS package dependency is required. When WPDS variables are absent, the static defaults above apply.

Recently-used commands

When showRecent is true (the default), the palette tracks which commands the user selects and shows them in a "Recently Used" group at the top when the search input is empty.

  • Recent entries are stored in localStorage under the key set by recentStorageKey (default: "@automattic/commands:recent").
  • Up to recentLimit entries are shown (default: 5).
  • Stale entries (commands whose id no longer exists in the commands array) are silently filtered out on read.
  • Storage failures (SSR, private browsing, disabled storage, quota exceeded) are swallowed — the hook degrades to an in-memory list for the session.
  • To disable, set showRecent={ false }.
  • To use separate recent lists per context, pass different recentStorageKey values.

Troubleshooting

The palette doesn't open

  • Verify the triggerKey matches what you expect. The default "Mod+k" maps to Cmd+K on macOS, Ctrl+K elsewhere.
  • Check that no other listener is calling event.preventDefault() on the same key combo before it reaches the palette.
  • Make sure <Commands /> is mounted in the component tree.

Route commands don't navigate

  • Confirm you passed onNavigate. Without it, resolved paths are silently discarded.
  • If the route has :param variables, make sure you also pass a resolver. Without one, params are listed as unresolved with no options.

Resolver errors

  • If the resolver throws or rejects, the error is logged to console.error and shown inside the palette. Check the console for [@automattic/commands] Route resolution failed:.
  • Press Backspace to dismiss the error and return to the command list, or press Escape to close the palette.
  • The resolver receives one param at a time. Return a string to auto-resolve it, or an array of strings to let the user choose.

"Command has an empty or missing id" warning

  • In development, commands are validated on mount. Each command must have a non-empty id and title, and exactly one of route or action.
  • Duplicate id values also trigger a warning. Validation is skipped in production.

Recent commands not persisting

  • localStorage must be available. In SSR or private browsing, the hook falls back to in-memory state.
  • Check that you're not passing a different recentStorageKey across renders.

CSS overrides not applying

  • Custom properties must be set on an ancestor of the dialog, or on :root. The palette renders via a portal, so scoped parent styles won't reach it — use :root or body.
  • Ensure your overrides load after the bundled stylesheet if specificity is equal.

ResizeObserver errors in tests

  • cmdk requires ResizeObserver. In jsdom, shim it before importing the component:
global.ResizeObserver = class {
	observe() {}
	unobserve() {}
	disconnect() {}
};

Development

nvm use
pnpm install
pnpm test
pnpm build

| Command | Description | | ------------------- | ----------------------------------------------------------------------------- | | pnpm build | Build ESM + CJS output to dist/ | | pnpm dev | Start the Vite playground with HMR against src/ at http://localhost:5173/ | | pnpm dev:dist | Build first, then start the playground against dist/ | | pnpm test | Run tests with Vitest and React Testing Library | | pnpm test:watch | Run tests in watch mode | | pnpm lint | Lint with ESLint | | pnpm lint:fix | Lint and auto-fix | | pnpm format | Format with Prettier | | pnpm format:check | Check formatting |

License

Licensed under GPL-2.0-or-later. See LICENSE.