@theryansmee/ngx-command-palette

July 17, 2026 ยท View on GitHub

npm version npm downloads license bundle size Angular docs

A keyboard-driven command palette for Angular. Routes are auto-registered from your Router config - zero setup required. Add custom commands, async search providers, contextual visibility, and full keyboard navigation out of the box.

Inspired by tools like Linear, GitHub, and Raycast.

Features

  • Auto-registers routes - walks your Angular Router config and creates searchable commands from every route with a title
  • Lazy-load aware - re-scans routes as lazy modules load
  • Async search providers - register API-backed search sources with per-provider debounce and loading states
  • Prefix routing - scope providers behind prefixes (@ for users, # for tickets) so they only fire when needed
  • Nested pages - commands with children open scoped submenu pages with breadcrumbs and Backspace navigation
  • Contextual commands - show or hide commands based on the current route or dynamic conditions
  • Fuzzy search - built-in scoring that ranks exact matches, prefix matches, word boundary matches, and character-by-character fuzzy matches
  • Keyword search - add extra search terms to any command
  • Recent commands - tracks recently used commands in localStorage with a configurable recency boost
  • Priority boosting - manually rank commands higher or lower
  • Keyboard navigation - Arrow keys, Enter, Escape, Tab - all handled
  • Accessible - follows the WAI-ARIA combobox pattern with role="combobox", aria-activedescendant, and focus trapping
  • Custom item templates - override row rendering globally or per-category with ng-template
  • Fully themeable - CSS custom properties for every visual aspect
  • SSR-safe - platform checks for localStorage and DOM APIs
  • Standalone components - no NgModule needed
  • Signal-based - reactive state using Angular signals

Angular Version Support

Each Angular major version is maintained on its own branch:

BranchAngularLibrarynpm tag
angular/1919.x19.x.xangular19
angular/2020.x20.x.xangular20
angular/2121.x21.x.xangular21
angular/2222.x22.x.xlatest

The main branch tracks the latest stable version.

To install a specific Angular version:

npm install @theryansmee/ngx-command-palette@angular21

Installation

ng add @theryansmee/ngx-command-palette

This automatically adds provideCommandPalette() to your app config, imports CmdPaletteComponent, and adds <cmd-palette /> to your root template.

Or install manually:

npm install @theryansmee/ngx-command-palette
# or
yarn add @theryansmee/ngx-command-palette
# or
pnpm add @theryansmee/ngx-command-palette

Peer Dependencies

PackageVersion
@angular/core^22.0.0
@angular/common^22.0.0
@angular/router^22.0.0
@angular/cdk^22.0.0

Quick Start

1. Provide the command palette

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideCommandPalette } from '@theryansmee/ngx-command-palette';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideCommandPalette(),
  ],
};

2. Add the component to your root template

<!-- app.component.html -->
<cmd-palette />
<router-outlet />

3. Add titles to your routes

// app.routes.ts
import { Routes } from '@angular/router';

export const routes: Routes = [
  { path: 'dashboard', component: DashboardComponent, title: 'Dashboard' },
  { path: 'settings', component: SettingsComponent, title: 'Settings' },
  { path: 'profile', component: ProfileComponent, title: 'Profile' },
];

That's it. Press Cmd+K (or Ctrl+K) and all your titled routes are searchable.

Configuration

Pass a config object to provideCommandPalette() to customize behavior:

provideCommandPalette({
  shortcut: 'mod.k',            // Keyboard shortcut to open (default: 'mod.k')
  placeholder: 'Search...',     // Input placeholder text (default: 'Search or type a command...')
  autoRegisterRoutes: true,     // Auto-register routes from Router config (default: true)
  maxResults: 10,               // Maximum search results shown (default: 10)
  trackRecent: true,            // Track recently used commands (default: false)
  recentCount: 5,               // Number of recent commands to track (default: 5)
  debounce: 150,                // Input debounce in milliseconds (default: 0)
  animation: 'scale',           // Open animation: 'scale' | 'slide' | 'none' (default: 'scale')
  theme: 'default',             // Built-in theme: 'default' | 'dark' | 'github' | 'linear'
  escapeBehavior: 'close',      // Escape inside a page: 'close' the palette or 'pop' one level (default: 'close', v22.2.0+)
});

Shortcut Format

The shortcut string uses dot-separated modifier keys followed by the key:

ShortcutKeys
mod.kCmd+K (Mac) / Ctrl+K (Windows/Linux)
meta.kCmd+K (Mac) / Win+K (Windows)
ctrl.kCtrl+K
ctrl.shift.pCtrl+Shift+P
meta.shift.pCmd+Shift+P
alt.shift.kAlt+Shift+K

Route Configuration

Basic Routes

Routes with a title property are auto-registered with no extra config:

{ path: 'dashboard', component: DashboardComponent, title: 'Dashboard' }
// -> Appears as "Dashboard" in the palette, navigates to /dashboard

Routes without a title still get registered - the label is generated from the path:

{ path: 'user-settings', component: UserSettingsComponent }
// -> Appears as "User Settings" in the palette

Enriching Routes

Add a commandPalette object to data to customize how a route appears:

{
  path: 'settings/billing',
  component: BillingComponent,
  title: 'Billing',
  data: {
    commandPalette: {
      label: 'Billing & Payments',           // Override the display label
      category: 'Settings',                   // Override the default "Pages" category
      keywords: ['invoice', 'payment', 'subscription'],  // Extra search terms
      priority: 5,                            // Higher = appears first
    },
  },
}

Excluding Routes

Set commandPalette to false to exclude a route:

{
  path: 'admin/debug',
  component: DebugComponent,
  title: 'Debug Panel',
  data: { commandPalette: false },
}

Parameterized Routes

Routes with parameters (e.g. :id) are automatically skipped unless you provide an explicit commandPalette config:

// This route is SKIPPED (has :id, no commandPalette config)
{ path: 'users/:id', component: UserDetailComponent, title: 'User Detail' }

// This route is INCLUDED (explicit config provided)
{
  path: 'users/:id',
  component: UserDetailComponent,
  title: 'User Detail',
  data: {
    commandPalette: {
      label: 'View User',
    },
  },
}

Wildcard and Redirect Routes

Wildcard (**) and redirect (redirectTo) routes are always excluded automatically.

Child Routes

Child routes are walked recursively and registered with their full path:

{
  path: 'admin',
  component: AdminComponent,
  title: 'Admin',
  children: [
    { path: 'users', component: AdminUsersComponent, title: 'Users' },
    { path: 'roles', component: AdminRolesComponent, title: 'Roles' },
  ],
}
// Registers: "Admin" (/admin), "Users" (/admin/users), "Roles" (/admin/roles)

Lazy-Loaded Routes

The palette automatically re-scans routes when lazy modules are loaded. Routes inside loadChildren become available once the module has been loaded at least once.

Custom Commands

Registering Commands

Inject CommandPaletteService and call register() to add custom commands:

import { Component, inject, DestroyRef } from '@angular/core';
import { CommandPaletteService } from '@theryansmee/ngx-command-palette';

@Component({ ... })
export class ProjectListComponent {
  readonly #palette = inject(CommandPaletteService);
  readonly #destroyRef = inject(DestroyRef);

  constructor() {
    this.#palette.register(
      [
        {
          id: 'create-project',
          label: 'Create New Project',
          category: 'Actions',
          shortcut: 'Cmd+N',
          keywords: ['new', 'add'],
          priority: 10,
          action: () => this.openCreateDialog(),
        },
        {
          id: 'export-csv',
          label: 'Export Projects as CSV',
          category: 'Actions',
          action: () => this.exportService.exportCSV(),
        },
      ],
      this.#destroyRef,  // Commands auto-deregister when the component is destroyed
    );
  }
}

Auto-Cleanup with DestroyRef

When you pass a DestroyRef as the second argument to register(), the commands are automatically deregistered when the component or service is destroyed. This is the recommended approach for component-scoped commands.

// Commands exist only while this component is alive
this.palette.register(commands, this.destroyRef);

Without a DestroyRef, commands persist until manually deregistered or the app is destroyed.

Contextual Commands

Commands can be scoped to specific routes or dynamic conditions using the context property:

this.palette.register(
  [
    {
      id: 'delete-project',
      label: 'Delete Project',
      category: 'Danger',
      action: () => this.deleteProject(),
      context: {
        routes: ['/projects/*'],       // Only visible on /projects/* pages
        when: () => this.canDelete(),  // And only when the user has permission
      },
    },
  ],
  this.destroyRef,
);

Context rules:

  • routes - an array of glob patterns matched against the current URL. Supports * (single segment) and ** (any depth).
  • when - a function that returns boolean. Re-evaluated each time the palette opens or the query changes.
  • If both are provided, both must pass for the command to be visible.
  • Commands without a context are always visible.

Command Interface

interface Command {
  id: string;                              // Unique identifier
  label: string;                           // Display text
  category?: string;                       // Group heading (e.g. "Pages", "Actions")
  icon?: string;                           // Icon name or identifier
  keywords?: string[];                     // Additional search terms
  shortcut?: string;                       // Display-only shortcut hint (e.g. "Cmd+N")
  action?: () => void | Promise<void>;     // What happens when the command is executed
  priority?: number;                       // Ranking boost (higher = appears first)
  context?: {
    routes?: string[];                     // Glob patterns for route visibility
    when?: () => boolean;                  // Dynamic visibility check
  };
  children?: CommandChildren;              // Turns the command into a submenu page (see Nested Pages, v22.2.0+)
  pagePlaceholder?: string;                // Input placeholder while this command's page is open (v22.2.0+)
  pageEmptyMessage?: string;               // Empty message while this command's page is open (v22.2.0+)
  data?: Record<string, unknown>;          // Arbitrary metadata for custom templates
}

A command needs an action, children, or both. With only children, selecting it opens its page. With both, the action runs first (analytics, cache priming) and then the page opens.

Async Search Providers

Register API-backed search sources that return results asynchronously. Results are merged with static commands and grouped by category.

Basic Provider (Universal)

A provider without a prefix fires on every query:

import { Component, inject, DestroyRef } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs';
import { CommandPaletteService } from '@theryansmee/ngx-command-palette';

@Component({ ... })
export class AppComponent {
  readonly #palette = inject(CommandPaletteService);
  readonly #destroyRef = inject(DestroyRef);
  readonly #http = inject(HttpClient);

  constructor() {
    this.#palette.registerProvider(
      {
        id: 'doc-search',
        category: 'Documentation',
        minQueryLength: 2,
        debounce: 300,
        search: (query) => this.#http.get<Doc[]>(`/api/docs?q=${query}`).pipe(
          map(docs => docs.map(doc => ({
            id: `doc:${doc.id}`,
            label: doc.title,
            action: () => window.open(doc.url),
          }))),
        ),
      },
      this.#destroyRef,
    );
  }
}

Prefixed Provider

A provider with a prefix only fires when the user types that prefix. This prevents unnecessary API calls when you have many providers.

When the user types a prefix character, it appears as a visual chip in the input and the placeholder updates to reflect the active provider. Pressing Backspace on an empty input exits prefix mode.

this.palette.registerProvider(
  {
    id: 'user-search',
    category: 'Users',
    prefix: '@',                    // Only fires when query starts with @
    placeholder: 'Search users...', // Shown in the input when this prefix is active
    emptyMessage: 'Start typing to search users...', // Shown when there are no results
    minQueryLength: 2,
    debounce: 300,
    search: (query) => this.userService.search(query).pipe(
      map(users => users.map(user => ({
        id: `user:${user.id}`,
        label: user.name,
        icon: 'person',
        action: () => this.router.navigate(['/users', user.id]),
      }))),
    ),
  },
  this.destroyRef,
);

this.palette.registerProvider(
  {
    id: 'ticket-search',
    category: 'Tickets',
    prefix: '#',                    // Only fires when query starts with #
    placeholder: 'Search tickets...',
    emptyMessage: 'Start typing to search tickets...',
    minQueryLength: 1,
    debounce: 200,
    search: (query) => this.ticketService.search(query).pipe(
      map(tickets => tickets.map(ticket => ({
        id: `ticket:${ticket.id}`,
        label: `${ticket.key}: ${ticket.title}`,
        action: () => this.router.navigate(['/tickets', ticket.id]),
      }))),
    ),
  },
  this.destroyRef,
);

With the above, typing @john only hits the user API, typing #billing only hits the ticket API, and typing dashboard only searches static commands. The prefix is stripped before being passed to the provider's search function.

Registered prefixes are automatically shown as hints in the palette footer.

SearchProvider Interface

interface SearchProvider {
  id: string;                                        // Unique identifier
  category: string;                                  // Group heading for results
  search: (query: string) => Observable<Command[]>;  // The search function
  prefix?: string;                                   // Prefix trigger (e.g. '@', '#')
  placeholder?: string;                              // Input placeholder when this prefix is active
  emptyMessage?: string;                             // Message shown when no results (default: 'No results found.')
  debounce?: number;                                 // Debounce in ms (default: 300)
  minQueryLength?: number;                           // Minimum chars before searching (default: 1)
  order?: number;                                    // Category sort order
}

Loading State

The palette shows a "Searching..." indicator while async providers are in-flight. You can also read the loading state programmatically:

const isLoading: boolean = this.palette.loading();

Nested Pages

Available from v22.2.0

Give a command children and it becomes a submenu: selecting it opens a scoped page instead of executing. The query clears, a breadcrumb chip appears in the input, and only that page's commands are searchable. Backspace on an empty input goes back one level; Escape closes the palette from any depth (set escapeBehavior: 'pop' in the config to make it go back one level instead, Raycast style). Closing always resets to the root.

children accepts three shapes:

ShapeBehaviourUse case
Command[]Fixed list, filtered locally"Change theme..."
() => Observable<Command[]>Fetched once when the page opens, then filtered locally"Assign to..." from a team list
{ provider: string }Every keystroke goes through that SearchProviderServer-side search

Static Children

this.palette.register([
  {
    id: 'theme',
    label: 'Change theme...',
    category: 'Preferences',
    pagePlaceholder: 'Pick a theme...',
    children: [
      { id: 'theme.light', label: 'Light', action: () => themeService.set('light') },
      { id: 'theme.dark', label: 'Dark', action: () => themeService.set('dark') },
      { id: 'theme.system', label: 'System', action: () => themeService.set('system') },
    ],
  },
]);

A trailing ... on the label is stripped from the breadcrumb chip, so "Change theme..." becomes the chip "Change theme".

Loaded Children

Pass a function returning an Observable<Command[]> and the palette fetches the children when the page opens, showing its loading indicator in the meantime:

{
  id: 'assign',
  label: 'Assign to...',
  context: { routes: ['/issues/*'] },
  pagePlaceholder: 'Search teammates...',
  pageEmptyMessage: 'Nobody matches that name.',
  children: () => this.userService.getTeamMembers().pipe(
    map(users => users.map(user => ({
      id: `assign.${user.id}`,
      label: user.name,
      action: () => this.issueService.assign(user.id),
    }))),
  ),
}

Children can have children of their own, so multi-step flows nest to any depth. Each level's closures carry the choices made so far. Loader results are cached until the palette closes; failed loads are not cached, so re-entering the page retries. Popping a page mid-load cancels the request.

Provider Children: Prefixes Are Bookmarked Pages

Point a command at a registered provider and both entry paths land on the same page. Typing the provider's prefix and selecting the command reach the identical search, placeholder, and empty state:

this.palette.registerProvider({
  id: 'users',
  category: 'People',
  prefix: '@',
  placeholder: 'Search people...',
  search: (query) => this.userService.search(query).pipe(map(toCommands)),
});

this.palette.register([
  {
    id: 'assign',
    label: 'Assign to...',
    children: { provider: 'users' },
  },
]);

This is the mental model for the whole feature: a typed prefix is just a fast path onto a provider page. Inside a page, typing a sigil is literal text; prefix detection only happens at the root.

Opening a Page Programmatically

openPage() opens the palette straight onto a page, for deep links like a toolbar button:

// By the id of a registered command with children
this.palette.openPage('assign');

// Or with a full page object
this.palette.openPage({
  id: 'quick-links',
  title: 'Quick Links',
  source: { kind: 'static', commands: quickLinkCommands },
});

Only top-level command ids can be resolved this way; nested pages are reached by navigating. openPage always resets the stack first, so a deep link never stacks on top of wherever the user was.

Page Behaviour Notes

  • Ordering: pages show all children in authored order at an empty query, so submenus keep positional muscle memory. Fuzzy ranking (with priority and recency boosts) kicks in once the user types.
  • Result limits: config.maxResults does not apply inside pages; a submenu is always exhaustive and the list scrolls. Cap a page explicitly with CommandPage.maxResults when using pushPage().
  • Command ids: child ids must be globally unique. Use a parent.child convention like theme.dark. The palette warns in dev mode when a static child id collides with a registered command id.
  • Recents: executing a child records it and its ancestor commands, so a frequently used submenu rises at the root over time.
  • Visibility: context.routes and context.when() filter children the same way they filter root commands.

Custom Item Templates

Available from v22.0.1

By default, each result row renders an icon, label, and shortcut badge. For richer results (avatars, status badges, descriptions, etc.) you can provide custom templates that override how items are rendered while keeping all keyboard navigation, accessibility, and active-state behavior intact.

Global Template

A template without a category value applies to all result items:

<cmd-palette>
  <ng-template cmdItemTemplate let-command let-active="active">
    <img [src]="command.data?.['avatar']" class="avatar" />
    <div class="details">
      <span>{{ command.label }}</span>
      <span class="description">{{ command.data?.['description'] }}</span>
    </div>
  </ng-template>
</cmd-palette>

Per-Category Template

Provide a category name to scope the template to a specific group. Items in other categories fall back to the default rendering:

<cmd-palette>
  <ng-template cmdItemTemplate="Jira Issues" let-command let-active="active">
    <span class="status-badge" [class]="command.data?.['status']">
      {{ command.data?.['status'] }}
    </span>
    <span>{{ command.label }}</span>
    <span class="ticket-id">{{ command.data?.['ticketId'] }}</span>
  </ng-template>

  <ng-template cmdItemTemplate="Team Members" let-command let-active="active">
    <img [src]="command.data?.['avatar']" class="avatar" />
    <span>{{ command.label }}</span>
    <span class="role">{{ command.data?.['role'] }}</span>
  </ng-template>
</cmd-palette>

Resolution Order

When rendering an item, the palette resolves its template in this order:

  1. Category-specific template matching the item's category
  2. Global template (one with no category value)
  3. Built-in default (icon + label + shortcut)

Template Context

Each template receives the following context variables:

VariableTypeAccessDescription
$implicitCommandlet-commandThe command object for this row
activebooleanlet-active="active"Whether this row is the currently selected item

Using the data Property

The data property on Command is an optional Record<string, unknown> for attaching arbitrary metadata that your custom templates can render. Search providers are the primary use case:

this.palette.registerProvider(
  {
    id: 'ticket-search',
    category: 'Jira Issues',
    search: (query) => this.ticketService.search(query).pipe(
      map(tickets => tickets.map(ticket => ({
        id: `ticket:${ticket.id}`,
        label: ticket.title,
        action: () => this.router.navigate(['/tickets', ticket.id]),
        data: {
          ticketId: ticket.key,
          status: ticket.status,
          assignee: ticket.assignee,
        },
      }))),
    ),
  },
  this.destroyRef,
);

Importing the Directive

The CmdItemTemplateDirective is standalone. Import it in the component where you use <cmd-palette>:

import { CmdPaletteComponent, CmdItemTemplateDirective } from '@theryansmee/ngx-command-palette';

@Component({
  imports: [CmdPaletteComponent, CmdItemTemplateDirective],
  // ...
})
export class AppComponent {}

Programmatic Control

const palette = inject(CommandPaletteService);

// Open the palette
palette.open();

// Open with a pre-filled query
palette.open('settings');

// Close the palette
palette.close();

// Toggle open/closed
palette.toggle();

// Update the search query
palette.updateQuery('dashboard');

// Execute a command programmatically
palette.execute(someCommand);

// Open the palette straight onto a page (see Nested Pages)
palette.openPage('assign');

// Navigate the page stack
palette.pushPage(somePage);
palette.popPage();
palette.goBack();

// Read current state (signals)
const isOpen: boolean = palette.isOpen();
const query: string = palette.query();
const results: ScoredCommand[] = palette.results();
const isLoading: boolean = palette.loading();
const currentPage: CommandPage | null = palette.currentPage();
const breadcrumbs: string[] = palette.breadcrumbs();

Keyboard Shortcuts

KeyAction
Cmd+K / Ctrl+KOpen the palette (configurable)
EscapeClose the palette; with escapeBehavior: 'pop', go back one page first
Arrow Down / TabMove selection down
Arrow UpMove selection up
EnterExecute the selected command, or open its page if it has children
Backspace (empty input)Go back one page, or exit prefix mode at the root

Search & Ranking

The built-in search engine uses a multi-signal scoring approach:

Scoring Breakdown

SignalScore RangeDescription
Exact label match100Query matches the label exactly
Label starts with query80Label begins with the query
Word boundary match60Query matches at a word boundary
Fuzzy substring match40Characters appear in order within the label
Fuzzy character match0-35Characters match with gaps (consecutive matches score higher)
Keyword matchCapped below labelKeywords contribute but never outrank a label match
Recent command boost+4 to +20Recently used commands get a boost (most recent = highest)
Priority boostpriority * 10Manual priority multiplier

When the query is empty, commands are sorted by priority (highest first) and limited to maxResults.

Theming

Built-in Themes

Four themes ship out of the box. Set via config for a permanent theme:

provideCommandPalette({
  theme: 'github',   // 'default' | 'dark' | 'github' | 'linear'
});

Or bind the theme input for dynamic runtime switching:

<cmd-palette [theme]="activeTheme()" />

Or apply a theme via CSS class on any ancestor element:

<body class="cmd-theme-dark">
  <cmd-palette />
</body>

When using the CSS class approach, import the theme file in your global styles:

@import '@theryansmee/ngx-command-palette/themes/dark.css';
ThemeDescriptionCSS file
defaultClean light themen/a (built-in)
darkNeutral dark background with light textthemes/dark.css
githubDark theme inspired by GitHub's command palettethemes/github.css
linearMinimal dark theme inspired by Linearthemes/linear.css

Animations

The palette opens with a configurable animation:

provideCommandPalette({
  animation: 'scale',   // 'scale' | 'slide' | 'none'
});

// Or dynamically via input:
// <cmd-palette [animation]="activeAnimation()" />

Animations are automatically disabled when the user has prefers-reduced-motion enabled.

CSS Custom Properties

The palette uses CSS custom properties for full visual control. Override any variable on the cmd-palette selector or a parent element:

cmd-palette {
  /* Backdrop */
  --cmd-backdrop: rgba(0, 0, 0, 0.5);

  /* Dialog */
  --cmd-bg: #ffffff;
  --cmd-border: #e2e8f0;
  --cmd-border-radius: 12px;
  --cmd-shadow: 0 16px 70px rgba(0, 0, 0, 0.2);
  --cmd-width: 640px;
  --cmd-max-height: 400px;

  /* Input */
  --cmd-input-padding: 16px;
  --cmd-input-font-size: 16px;
  --cmd-input-color: #1a1a1a;
  --cmd-input-placeholder: #64748b;

  /* Prefix chip (shown when a prefixed provider is active) */
  --cmd-prefix-chip-bg: #e2e8f0;
  --cmd-prefix-chip-color: #475569;

  /* Items */
  --cmd-item-padding: 10px 16px;
  --cmd-item-color: #334155;
  --cmd-item-hover-bg: #f1f5f9;
  --cmd-item-active-bg: #e2e8f0;

  /* Group headings */
  --cmd-group-heading-color: #64748b;
  --cmd-group-heading-size: 12px;

  /* Shortcut badges */
  --cmd-shortcut-bg: #f1f5f9;
  --cmd-shortcut-color: #64748b;
  --cmd-shortcut-border: #e2e8f0;

  /* Empty state */
  --cmd-empty-color: #64748b;
}

Dark Theme Example

.dark cmd-palette,
cmd-palette.dark {
  --cmd-backdrop: rgba(0, 0, 0, 0.7);
  --cmd-bg: #1e1e2e;
  --cmd-border: #313244;
  --cmd-shadow: 0 16px 70px rgba(0, 0, 0, 0.5);
  --cmd-input-color: #cdd6f4;
  --cmd-input-placeholder: #6c7086;
  --cmd-item-color: #cdd6f4;
  --cmd-item-hover-bg: #313244;
  --cmd-item-active-bg: #45475a;
  --cmd-group-heading-color: #a6adc8;
  --cmd-shortcut-bg: #313244;
  --cmd-shortcut-color: #a6adc8;
  --cmd-shortcut-border: #45475a;
  --cmd-empty-color: #6c7086;
}

Accessibility

The palette follows the WAI-ARIA combobox pattern out of the box with no configuration needed. Focus trapping, focus restoration, screen reader announcements, keyboard navigation, and active item scrolling all work automatically.

API Reference

provideCommandPalette(config?)

Environment provider factory. Call in your appConfig.providers array.

OptionTypeDefaultDescription
shortcutstring'mod.k'Keyboard shortcut to open the palette
placeholderstring'Search or type a command...'Input placeholder text
autoRegisterRoutesbooleantrueAuto-register routes from Router config
maxResultsnumber10Maximum results shown
trackRecentbooleanfalseTrack recently used commands and boost them in results
recentCountnumber5Number of recent commands tracked (requires trackRecent: true)
debouncenumber0Input debounce in milliseconds
animation'scale' | 'slide' | 'none''scale'Open animation style
theme'default' | 'dark' | 'github' | 'linear''default'Built-in colour theme

CommandPaletteService

The main service for interacting with the palette.

MethodSignatureDescription
open(initialQuery?: string) => voidOpens the palette, optionally with a pre-filled query
close() => voidCloses the palette and clears the query
toggle() => voidToggles the palette open/closed
updateQuery(query: string) => voidUpdates the search query
execute(command: Command) => voidExecutes a command (or opens its page), records it as recent, and closes
register(commands: Command[], destroyRef?: DestroyRef) => voidRegisters static commands with optional auto-cleanup
registerProvider(provider: SearchProvider, destroyRef?: DestroyRef) => voidRegisters an async search provider with optional auto-cleanup
openPage(commandIdOrPage: string | CommandPage) => voidOpens the palette straight onto a page, resetting any existing stack (v22.2.0+)
pushPage(page: CommandPage) => voidPushes a page onto the stack and clears the query (v22.2.0+)
popPage() => voidPops the top page and clears the query (v22.2.0+)
goBack() => voidPops one page, or exits prefix mode at the root (v22.2.0+)
SignalTypeDescription
isOpenSignal<boolean>Whether the palette is currently open
querySignal<string>The current search query (including prefix)
displayQuerySignal<string>The query with the active prefix stripped
resultsSignal<ScoredCommand[]>The current search results (scored and sorted)
loadingSignal<boolean>Whether an async provider or page loader is currently working
activeProviderSignal<SearchProvider | null>The provider behind the current provider page (typed prefix or pushed page)
activePlaceholderSignal<string>The current input placeholder (page-specific, provider-specific, or default)
emptyMessageSignal<string>The current empty state message (page-specific, provider-specific, or default)
currentPageSignal<CommandPage | null>The active page, or null at the root (v22.2.0+)
breadcrumbsSignal<string[]>Titles of the open pages, or the active prefix at the root (v22.2.0+)

CmdPaletteComponent

The root component. Add it once in your app root template.

<cmd-palette />
InputTypeDefaultDescription
themeCommandPaletteThemeconfig valueOverride the theme for this instance
animationCommandPaletteAnimationconfig valueOverride the animation for this instance

Content projection: Place <ng-template cmdItemTemplate> elements inside <cmd-palette> to customize item rendering. See Custom Item Templates.

CmdItemTemplateDirective

Available from v22.0.1

Structural directive used on <ng-template> to define custom item templates.

<ng-template cmdItemTemplate let-command let-active="active">...</ng-template>
<ng-template cmdItemTemplate="Category Name" let-command let-active="active">...</ng-template>
InputTypeDefaultDescription
cmdItemTemplatestring''Category name to scope the template to. Empty string applies to all items.

CmdItemTemplateContext

Available from v22.0.1

The type-safe context interface for custom item templates.

interface CmdItemTemplateContext {
  $implicit: Command;  // Available via let-command (or any variable name)
  active: boolean;     // Available via let-active="active"
}

Migrating from @ngxpert/cmdk

If you're coming from @ngxpert/cmdk (or @ngneat/cmdk), this section maps the concepts you already know to their equivalents in ngx-command-palette.

Setup

With @ngxpert/cmdk, setup involves importing 7+ components and directives, building a custom template, wiring up Angular CDK Overlay for the dialog, and adding a @HostListener for the keyboard shortcut. With ngx-command-palette, the same result takes two lines:

// app.config.ts
provideCommandPalette()

// app.component.html
<cmd-palette />

The overlay, keyboard shortcut, focus trapping, backdrop, and animations are all built in.

Component and Directive Mapping

@ngxpert/cmdkngx-command-paletteNotes
<cmdk-command><cmd-palette />Includes overlay, shortcut listener, and all subcomponents
<input cmdkInput>Built-inNo separate import needed
<cmdk-list>Built-inResults list is managed internally
<cmdk-group label="...">category on CommandGroups are created automatically from the category property
<button cmdkItem>Command objectItems are registered via CommandPaletteService.register() instead of template markup
<div *cmdkEmpty>Built-inCustomizable per-provider via emptyMessage
<cmdk-separator>Built-inAutomatic separators between category groups
<div *cmdkLoader>Built-inLoading indicator shows automatically during async searches
[filter] on <cmdk-command>Built-in fuzzy scoringMulti-signal ranking with exact, prefix, boundary, fuzzy, keyword, recency, and priority signals

Defining Items

@ngxpert/cmdk defines items as template markup with a directive:

<cmdk-group label="Actions">
  <button cmdkItem [value]="'create'" (selected)="onCreate()">Create Project</button>
  <button cmdkItem [value]="'export'" (selected)="onExport()">Export CSV</button>
</cmdk-group>

ngx-command-palette defines items as data objects registered through a service:

this.palette.register([
  {
    id: 'create',
    label: 'Create Project',
    category: 'Actions',
    action: () => this.onCreate(),
  },
  {
    id: 'export',
    label: 'Export CSV',
    category: 'Actions',
    action: () => this.onExport(),
  },
], this.destroyRef);

This data-driven approach enables features that template-based items cannot support: fuzzy search with scoring, automatic route registration, keyword matching, contextual visibility, recency tracking, and priority boosting.

Custom Filtering

@ngxpert/cmdk accepts a custom filter function on the root component:

<cmdk-command [filter]="myFilter">

ngx-command-palette does not expose a custom filter hook because the built-in search engine handles scoring across multiple signals (exact match, prefix, word boundary, fuzzy, keywords, recency, priority). For API-backed search, use async search providers instead.

Dialog and Keyboard Shortcut

@ngxpert/cmdk requires you to wire up the dialog overlay and keyboard shortcut yourself, typically using Angular CDK Overlay and @HostListener:

// @ngxpert/cmdk - manual setup required
@HostListener('document:keydown.meta.k', ['$event'])
openDialog(event: KeyboardEvent): void {
  event.preventDefault();
  this.isOpen = true;
}

ngx-command-palette handles this automatically. Configure the shortcut in the provider:

provideCommandPalette({ shortcut: 'mod.k' })

Route Registration

This is the biggest difference. @ngxpert/cmdk has no concept of routes. Every item must be manually added to the template.

ngx-command-palette auto-registers all titled routes from your Angular Router config. Set autoRegisterRoutes: true (the default) and every route with a title becomes searchable with zero additional code. See Route Configuration for details.

What You Gain

Switching from @ngxpert/cmdk to ngx-command-palette gives you:

  • Automatic route registration with lazy-load awareness
  • Fuzzy search with multi-signal scoring instead of basic substring matching
  • Async search providers with per-provider debounce, prefix routing, and loading states
  • Contextual commands scoped to routes or dynamic conditions
  • Recent command tracking with recency boosting
  • Built-in dialog, shortcut, and focus management with no CDK Overlay boilerplate
  • Four built-in themes with full CSS custom property support
  • WAI-ARIA combobox pattern implemented out of the box
  • Custom item templates for rich result rendering
  • Active maintenance across Angular 19, 20, 21, and 22

License

MIT