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

@angularforge/command-palette

v0.0.1

Published

A professional, accessible command palette for Angular 20+. VS Code / Linear / Raycast-style global search, fuzzy matching, keyboard shortcuts, and Angular Signals support.

Readme

angularforge


Playground

Explore and customize the command palette in real time using the interactive Playground.

Features

Devices Views

Table of Contents


Features

  • Keyboard-firstCmd+K / Ctrl+K (or any custom shortcut) to open/close
  • Fuzzy search — intelligent ranking with consecutive-character bonuses
  • Grouped results — organise commands by category, navigated in visual order
  • Full keyboard navigation↑ ↓ Home End Enter Esc, with auto-scroll to the active item
  • ng-icons support — Heroicons, Lucide, Bootstrap Icons (and any other ng-icons pack) via ngIcon
  • Theming — CSS custom properties for every visual token; light & dark mode out of the box
  • CDK Overlay — scroll blocking via BlockScrollStrategy
  • CDK FocusTrap — keyboard focus locked inside the dialog
  • WCAG AA — ARIA combobox/listbox pattern, focus trap, screen reader support
  • OnPush everywhere — all components use ChangeDetectionStrategy.OnPush
  • Signal-first — built with Angular Signals, computed(), and effect() (zero RxJS in state)
  • Standalone components — zero NgModule, fully tree-shakable
  • SSR & Zoneless compatible — no direct window/document access in state
  • Tree-shakablesideEffects: false

Requirements

| Dependency | Version | | --------------------------- | ---------- | | @angular/core | >=20.0.0 | | @angular/common | >=20.0.0 | | @angular/cdk | >=20.0.0 | | @angular/platform-browser | >=20.0.0 | | @ng-icons/core (optional) | >=29.0.0 |


Installation

npm install @angularforge/command-palette @angular/cdk

To use icons from ng-icons, install the core package and any icon packs you need:

# Core (required for icon support)
npm install @ng-icons/core

# Icon packs — install only what you use
npm install @ng-icons/heroicons       # Heroicons (outline · solid · mini · micro)
npm install @ng-icons/lucide          # Lucide
npm install @ng-icons/bootstrap-icons # Bootstrap Icons

Quick Start

1. Add the provider

// main.ts
import { bootstrapApplication } from "@angular/platform-browser";
import { provideCommandPalette } from "@angularforge/command-palette";
import { AppComponent } from "./app/app.component";

bootstrapApplication(AppComponent, {
  providers: [provideCommandPalette()],
});

2. Add the component to AppComponent

import { Component } from "@angular/core";
import { CommandPaletteComponent } from "@angularforge/command-palette";

@Component({
  selector: "app-root",
  imports: [CommandPaletteComponent],
  template: `
    <router-outlet />
    <af-command-palette />
  `,
})
export class AppComponent {}

3. Register commands anywhere

import { Component, DestroyRef, inject, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { CommandPaletteService } from "@angularforge/command-palette";

@Component({
  /* ... */
})
export class SidebarComponent implements OnInit {
  private readonly palette = inject(CommandPaletteService);
  private readonly router = inject(Router);
  private readonly destroyRef = inject(DestroyRef);

  ngOnInit(): void {
    this.palette.register([
      {
        id: "nav-home",
        label: "Home",
        description: "Go to the home page",
        category: "Navigation",
        icon: "🏠",
        keywords: ["start", "landing"],
        action: () => this.router.navigate(["/"]),
      },
      {
        id: "nav-users",
        label: "Users",
        category: "Navigation",
        shortcut: "⌘U",
        action: () => this.router.navigate(["/users"]),
      },
      {
        id: "action-create",
        label: "Create User",
        description: "Open the user creation form",
        category: "Actions",
        action: () => this.router.navigate(["/users/new"]),
      },
    ]);

    this.destroyRef.onDestroy(() =>
      this.palette.unregister(["nav-home", "nav-users", "action-create"]),
    );
  }
}

Press Cmd+K or Ctrl+K — the palette opens and your commands appear.


Icon Integration (ng-icons)

@angularforge/command-palette has optional first-class support for ng-icons. Icons are registered once at the app level and then referenced by name on each command — fully tree-shakable.

1. Register icons with provideCommandPaletteIcons()

Add provideCommandPaletteIcons() next to provideCommandPalette() and pass only the icons your app uses:

// app.config.ts
import {
  provideCommandPalette,
  provideCommandPaletteIcons,
} from "@angularforge/command-palette";

// Heroicons (outline variant)
import {
  heroMagnifyingGlass,
  heroCommandLine,
  heroHome,
} from "@ng-icons/heroicons/outline";
// Lucide
import { lucideSettings, lucideUsers } from "@ng-icons/lucide";
// Bootstrap Icons
import {
  bootstrapTerminal,
  bootstrapFileText,
} from "@ng-icons/bootstrap-icons";

export const appConfig = {
  providers: [
    provideCommandPalette(),
    provideCommandPaletteIcons({
      heroMagnifyingGlass,
      heroCommandLine,
      heroHome,
      lucideSettings,
      lucideUsers,
      bootstrapTerminal,
      bootstrapFileText,
    }),
  ],
};

2. Use ngIcon on commands

Set the ngIcon field to the icon name string (matches the exported variable name):

this.palette.register([
  {
    id: "nav-home",
    label: "Home",
    category: "Navigation",
    ngIcon: "heroHome",
    action: () => this.router.navigate(["/"]),
  },
  {
    id: "nav-users",
    label: "Users",
    category: "Navigation",
    ngIcon: "lucideUsers",
    action: () => this.router.navigate(["/users"]),
  },
  {
    id: "action-terminal",
    label: "Open Terminal",
    category: "Tools",
    ngIcon: "bootstrapTerminal",
    action: () => openTerminal(),
  },
]);

Icon pack prefixes

| Pack | Install | Prefix | Example | | ----------------- | --------------------------- | ----------- | --------------------- | | Heroicons Outline | @ng-icons/heroicons | hero | heroMagnifyingGlass | | Heroicons Solid | @ng-icons/heroicons | heroSolid | heroSolidHome | | Lucide | @ng-icons/lucide | lucide | lucideSearch | | Bootstrap Icons | @ng-icons/bootstrap-icons | bootstrap | bootstrapTerminal |

Import from the correct sub-path for Heroicons:

import { heroMagnifyingGlass } from "@ng-icons/heroicons/outline"; // outline (default)
import { heroSolidHome } from "@ng-icons/heroicons/solid"; // solid
import { heroMiniChevronDown } from "@ng-icons/heroicons/mini"; // mini (20px)
import { heroMicroCheck } from "@ng-icons/heroicons/micro"; // micro (16px)

Fallback priority

Each command item resolves its icon in this order:

  1. ngIcon — ng-icons icon by name
  2. icon — raw SVG/HTML string via innerHTML
  3. Built-in dot placeholder

You can mix both fields across commands in the same palette. When both icon and ngIcon are provided, ngIcon takes precedence.


Configuration

provideCommandPalette({
  /** Keyboard shortcut(s) to open the palette. Default: ['meta+k', 'ctrl+k'] */
  shortcut: "meta+k",

  /** Close when Escape is pressed. Default: true */
  closeOnEscape: true,

  /** Enable fuzzy character matching. Default: true */
  fuzzySearch: true,

  /** Placeholder text inside the search input. Default: 'Search commands…' */
  placeholder: "Search commands…",

  /** Maximum visible results. Default: 50 */
  maxResults: 50,
});

Multiple shortcuts

provideCommandPalette({
  shortcut: ["meta+k", "ctrl+k", "meta+shift+p"],
});

Shortcut tokens are case-insensitive and accept aliases: meta/cmd, ctrl/control, alt/option, and shift — e.g. 'cmd+shift+p'.


API Reference

provideCommandPalette(config?)

Returns EnvironmentProviders. Call once in bootstrapApplication or a route's providers array. Accepts a partial CommandPaletteConfig; any omitted option falls back to its default.

provideCommandPaletteIcons(icons)

Registers ng-icons icon definitions for the palette. Call alongside provideCommandPalette(). Pass a record of icon name → icon SVG value — import these from the relevant @ng-icons/* pack.

provideCommandPaletteIcons({
  heroMagnifyingGlass,
  lucideSearch,
  bootstrapTerminal,
});

CommandPaletteService

Inject anywhere to control the palette or register commands.

| Method / Signal | Type | Description | | -------------------- | ------------------------------- | ---------------------------------------------------------------------------- | | isOpen | Signal<boolean> | Current open state | | query | Signal<string> | Current search query | | results | Signal<SearchResult[]> | Flat, score-ranked results | | groups | Signal<CommandGroup[]> | Results grouped by category | | orderedResults | Signal<SearchResult[]> | Results flattened in rendered (grouped) order — used for keyboard navigation | | hasResults | Signal<boolean> | Shorthand for results().length > 0 | | open() | void | Open the palette and reset the query | | close() | void | Close the palette | | toggle() | void | Toggle open/closed | | register(commands) | (commands: Command[]) => void | Add or update commands by id | | unregister(ids) | (ids: string[]) => void | Remove commands by id | | setQuery(query) | (query: string) => void | Programmatically set the search query |

CommandPaletteComponent

Host component — place once in AppComponent. Selector: <af-command-palette>.

| Input | Type | Default | Description | | ------------- | -------- | ------- | ------------------------------------------- | | footerClass | string | '' | Extra CSS class(es) added to the footer bar |

Command

interface Command {
  id: string;
  label: string;
  description?: string;
  keywords?: string[];
  /** Raw SVG/HTML string rendered via innerHTML. */
  icon?: string;
  /** ng-icons icon name — e.g. 'heroMagnifyingGlass', 'lucideSearch', 'bootstrapTerminal'. */
  ngIcon?: string;
  category?: string; // Group heading (default: 'Commands')
  disabled?: boolean;
  shortcut?: string; // Display string only — e.g. '⌘K'
  action: () => void | Promise<void>;
}

CommandPaletteConfig

interface CommandPaletteConfig {
  shortcut?: string | string[]; // default: ['meta+k', 'ctrl+k']
  closeOnEscape?: boolean; // default: true
  fuzzySearch?: boolean; // default: true
  placeholder?: string; // default: 'Search commands…'
  maxResults?: number; // default: 50
}

SearchResult & CommandGroup

interface SearchResult {
  command: Command;
  score: number; // see the Fuzzy Search Engine scoring tiers
}

interface CommandGroup {
  category: string;
  results: SearchResult[];
}

Examples

Basic Setup

// main.ts
bootstrapApplication(AppComponent, {
  providers: [provideCommandPalette()],
});
<!-- app.component.html -->
<router-outlet />
<af-command-palette />

Router Navigation

Call router.navigate() inside a command's action:

this.palette.register([
  {
    id: "go-settings",
    label: "Settings",
    category: "Navigation",
    action: () => this.router.navigate(["/settings"]),
  },
  {
    id: "go-profile",
    label: "Profile",
    category: "Navigation",
    action: () => this.router.navigate(["/profile"]),
  },
]);

Custom Shortcuts

provideCommandPalette({
  shortcut: ["meta+k", "ctrl+k", "meta+shift+p"],
  placeholder: "Type a command…",
});

Async Actions

action may return a Promise; the palette closes immediately and awaits it:

this.palette.register([
  {
    id: "sync-now",
    label: "Sync Now",
    category: "Actions",
    action: async () => {
      await this.api.sync();
    },
  },
]);

Programmatic Control

const palette = inject(CommandPaletteService);

palette.open(); // open + reset query
palette.setQuery("users"); // prefill the search box
palette.toggle(); // open/close
palette.close();

Lazy / Scoped Commands

Register commands from any feature component and clean them up on destroy:

ngOnInit() {
  this.palette.register(this.featureCommands);
  this.destroyRef.onDestroy(() =>
    this.palette.unregister(this.featureCommands.map((c) => c.id))
  );
}

Theming

All visual tokens are CSS custom properties. Override them on the af-command-palette element or in your global CSS:

af-command-palette {
  /* Dialog */
  --af-command-palette-bg: #ffffff;
  --af-command-palette-color: #0f172a;
  --af-command-palette-border: rgba(0, 0, 0, 0.1);
  --af-command-palette-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.4);
  --af-command-palette-radius: 14px;
  --af-command-palette-width: 640px;

  /* Backdrop */
  --af-command-palette-backdrop: rgba(0, 0, 0, 0.45);
  --af-command-palette-backdrop-blur: 4px;

  /* Text & icons */
  --af-command-palette-muted: #94a3b8;

  /* Active item */
  --af-command-palette-item-active-bg: rgba(99, 102, 241, 0.1);
  --af-command-palette-item-active-color: #4f46e5;
  --af-command-palette-item-radius: 8px;

  /* Keyboard hints */
  --af-command-palette-kbd-bg: rgba(0, 0, 0, 0.05);

  /* Footer */
  --af-command-palette-footer-bg: rgba(248, 250, 252, 0.8);

  /* Focus ring */
  --af-command-palette-focus-ring: #6366f1;
}

Dark mode is handled automatically via prefers-color-scheme: dark. To force a theme, set the CSS variables on a wrapper element or pass an explicit class.

Example: brand colours

af-command-palette {
  --af-command-palette-item-active-bg: rgba(16, 185, 129, 0.12);
  --af-command-palette-item-active-color: #059669;
  --af-command-palette-focus-ring: #10b981;
}

Theming the footer

The footer bar (keyboard hints) is styled via --af-command-palette-footer-bg and the shared border/muted tokens. For deeper overrides, use the footerClass input to inject a custom class:

<af-command-palette footerClass="my-footer" />
/* The base .af-cp-footer class stays; your class adds on top of it */
.my-footer {
  background: #1e1e2e;
  border-top-color: #313244;
}

.my-footer .af-cp-footer-hint kbd {
  background: #313244;
  border-color: #45475a;
  color: #cdd6f4;
}

Multiple classes are supported — separate them with spaces:

<af-command-palette footerClass="my-footer my-footer--compact" />

Fuzzy Search Engine

The built-in FuzzySearchEngine is exported so you can use it standalone or extend it. It searches across each command's label, description, category, and keywords, and ignores disabled commands.

import { FuzzySearchEngine } from "@angularforge/command-palette";

const engine = new FuzzySearchEngine();
const results = engine.search(commands, "usr"); // fuzzy enabled
const strict = engine.search(commands, "usr", false); // substring-only

Scoring tiers (highest wins):

| Score | Match type | | ----- | ------------------------------------------ | | 1000 | Exact match | | 800 | Target starts with query | | 600 | A word within the target starts with query | | 400 | Target contains query | | 1–199 | Fuzzy consecutive-character match |

You can also call the keyboard helpers directly:

import {
  parseShortcut,
  matchesShortcut,
  matchesAnyShortcut,
} from "@angularforge/command-palette";

matchesAnyShortcut(event, ["meta+k", "ctrl+k"]); // boolean

Architecture

Directory Structure

src/
├── public-api.ts                            ← Public API surface
└── lib/
    ├── components/
    │   ├── index.ts
    │   ├── command-palette/
    │   │   └── command-palette.component.ts  ← Host: dialog, backdrop, footer, global shortcut
    │   ├── palette-input/
    │   │   └── palette-input.component.ts     ← Search input (combobox)
    │   ├── palette-results/
    │   │   └── palette-results.component.ts   ← Scrollable listbox
    │   ├── palette-group/
    │   │   └── palette-group.component.ts     ← One category section
    │   └── palette-item/
    │       └── palette-item.component.ts      ← One command row (option)
    ├── keyboard/
    │   ├── index.ts
    │   └── shortcut.parser.ts                 ← parse/match keyboard shortcuts
    ├── models/
    │   ├── index.ts
    │   └── command.model.ts                   ← Command / Config / SearchResult / CommandGroup
    ├── providers/
    │   ├── index.ts
    │   ├── command-palette.provider.ts        ← provideCommandPalette()
    │   └── command-palette-icons.provider.ts  ← provideCommandPaletteIcons()
    ├── search/
    │   ├── index.ts
    │   └── fuzzy-search.engine.ts             ← Dependency-free fuzzy ranking
    ├── services/
    │   ├── index.ts
    │   └── command-palette.service.ts         ← Signal-based state hub
    └── tokens/
        ├── index.ts
        └── command-palette.tokens.ts          ← Config injection token + defaults

Component Tree

CommandPaletteComponent  (selector: af-command-palette)
│   • host (document:keydown) → open shortcut + Escape
│   • CDK Overlay BlockScrollStrategy + CDK cdkTrapFocus
│   • reads state from CommandPaletteService (providedIn: 'root')
├── PaletteInputComponent        (af-palette-input — role="combobox")
└── PaletteResultsComponent      (af-palette-results — role="listbox")
    └── PaletteGroupComponent × N (af-palette-group — role="group")
        └── PaletteItemComponent × N (af-palette-item — role="option")

Accessibility

@angularforge/command-palette is built to the WCAG AA standard:

| Feature | Status | | ------------------------------------------ | ----------------------------------------------- | | ARIA combobox/listbox pattern | Supported (WAI-ARIA 1.2 §3.7) | | role="dialog" + aria-modal="true" | On the container | | aria-activedescendant | Tracks the active option | | role="option" + aria-selected | On every item | | role="group" + aria-labelledby | On every category section | | CDK FocusTrap | Tab / Shift+Tab cycles within the dialog | | Keyboard navigation (↑ ↓ Home End Enter) | Auto-scrolls the active item into view | | Escape closes & restores focus | Returns focus to the previously focused element |


Performance

  • OnPush change detection on every component — Angular only checks when inputs change or signals emit
  • Signals & computed — reactive state with no BehaviorSubject/RxJS in state management
  • @for track — keyed by command id, prevents full list re-renders
  • maxResults cap — limits the rendered list for large command sets
  • Dependency-free fuzzy engine — no extra runtime dependencies
  • sideEffects: false in package.json — enables full tree-shaking

FAQ

Can I open the palette programmatically? Yes — inject CommandPaletteService and call service.open().

Can I register commands lazily (from a feature component)? Yes — register in any component or service, and call unregister in DestroyRef.onDestroy.

Does it work with SSR / Angular Universal? Yes. State has no direct window/document access, and CDK handles platform detection internally.

Does it work in Zoneless apps? Yes. All state is Signal-based; there is no detectChanges or Zone dependency.

Can I disable fuzzy matching? Yes — provideCommandPalette({ fuzzySearch: false }).

Can I use ng-icons packs other than Heroicons, Lucide, and Bootstrap? Yes — provideCommandPaletteIcons() accepts any Record<string, string> and delegates to provideIcons() from @ng-icons/core.

Can I have multiple palettes? CommandPaletteService is providedIn: 'root'. For scoped palettes, provide the service at a lower injector level.


Local Development & Testing

This package is built with ng-packagr. The output goes to ./dist.

npm run build         # build the library to ./dist
npm run build:watch   # rebuild on change

Testing it in another project (avoid the NG0203 trap)

Do not use npm link to test the library locally. Linking makes the bundler resolve the library's @angular/core to its own node_modules, loading two copies of Angular, which triggers:

NG0203: inject() must be called from an injection context …

Instead, install a packed tarball — it has no nested Angular and behaves exactly like a real npm install:

# In this library
npm run build
npm pack            # → angularforge-command-palette-<version>.tgz

# In the consuming app
npm install /path/to/command-palette/angularforge-command-palette-<version>.tgz

If you must use npm link, set "preserveSymlinks": true in the consuming app's angular.json build options so it loads a single Angular instance.


Contributing

# Clone the repo
git clone https://github.com/angularforge/command-palette
cd command-palette

# Install dependencies
npm install

# Build the library (ng-packagr)
npm run build

Please follow Conventional Commits for all commit messages.


Stay in touch


Support

If this library saved you time, consider buying me a coffee:

Donate via PayPal


License

MIT © AngularForge


Forged with ⚒️❤️‍🔥 for the Angular community 🚀