Command Palette

The <smart-command-palette> component provides a Cmd+K-style overlay that filters a list of commands by a search query. It follows the Base + Standard + Wrapper pattern with an InjectionToken-based extension mechanism. The abstract CommandPaletteBaseComponent defines the shared API — commands (InputSignal<ICommand[]>), two-way open (ModelSignal<boolean>) and query (ModelSignal<string>), optional ICommandPaletteOptions, cssClass (alias class), a filteredCommands computed signal that performs case-insensitive substring matching on command.label, plus selectCommand(id) and close() behaviors. CommandPaletteStandardComponent is a barebones placeholder concrete implementation. CommandPaletteComponent is the public wrapper that renders CommandPaletteStandardComponent by default and accepts a custom replacement via COMMAND_PALETTE_STANDARD_COMPONENT_TOKEN.


Usage

<button type="button" (click)="open.set(true)">Open command palette</button>

<smart-command-palette
  [commands]="commands"
  [options]="options"
  [(open)]="open"
  [(query)]="query"
  (runCommand)="onRunCommand($event)"
/>

Components

CommandPaletteComponent (<smart-command-palette>)

Main wrapper component. Renders CommandPaletteStandardComponent by default. When COMMAND_PALETTE_STANDARD_COMPONENT_TOKEN is provided, renders the injected component via NgComponentOutlet.

CommandPaletteStandardComponent (<smart-command-palette-standard>)

Barebones placeholder concrete implementation. Renders a native <dialog [open]> containing an <input type="search"> bound to query and a <ul role="listbox"> with one <li role="option"> per filtered command. It does not include Tailwind UI styling — it exists solely as the default structural placeholder until a custom implementation is registered through the token. Keyboard handling for Escape (close) and the Cmd+K global shortcut (open) is intentionally deferred to custom implementations.

CommandPaletteBaseComponent (abstract)

Abstract base directive for extending custom command palette implementations. Exposes commands as an InputSignal<ICommand[]> (default []), open and query as two-way ModelSignals (defaults false / ''), options as an InputSignal<ICommandPaletteOptions | undefined>, cssClass as an InputSignal<string> (with alias class), a filteredCommands computed (case-insensitive label substring), a selectCommand(commandId) method that emits the runCommand output and sets open to false, and a close() method that sets open to false.

API

Inputs

InputTypeDefaultDescription
commandsInputSignal<ICommand[]>[]List of commands available in the palette
openModelSignal<boolean>falseVisibility state (two-way bindable)
queryModelSignal<string>''Search query (two-way bindable)
optionsInputSignal<ICommandPaletteOptions | undefined>-Optional configuration
classInputSignal<string>''External CSS classes (alias for cssClass)

Outputs

OutputPayloadDescription
runCommand{ commandId: string }Fired when a user selects a command

ICommandPaletteOptions

The standard component only consumes placeholder, emptyText, and ariaLabel. The variant selector is reserved for custom implementations registered through COMMAND_PALETTE_STANDARD_COMPONENT_TOKEN.

COMMAND_PALETTE_STANDARD_COMPONENT_TOKEN

InjectionToken that allows replacing the default CommandPaletteStandardComponent with a custom implementation. Provide a Type<CommandPaletteBaseComponent> to override.

Extending the base class

import {
  ChangeDetectionStrategy,
  Component,
  computed,
  input,
  ViewEncapsulation,
} from '@angular/core';

import {
  CommandPaletteBaseComponent,
  CommandPaletteComponent,
  COMMAND_PALETTE_STANDARD_COMPONENT_TOKEN,
  ICommand,
  ICommandPaletteOptions,
} from '@smartsoft001/angular';

/**
 * A custom command palette built on `CommandPaletteBaseComponent`.
 *
 * The base owns the `commands` input, the `open` and `query` models, the
 * `filteredCommands` computed and `selectCommand()` / `close()`. Note that the
 * base binds no keyboard listeners: handling Escape or a global Cmd+K is the
 * implementation's job. Call `selectCommand()` rather than emitting
 * `runCommand` by hand, because it also closes the palette.
 */
@Component({
  selector: 'docs-custom-command-palette',
  template: `
    <div [class]="containerClasses()" [hidden]="!open()">
      <input
        type="search"
        class="docs-command-palette__search"
        [value]="query()"
        [attr.placeholder]="options()?.placeholder ?? null"
        (input)="onQueryChange($event)"
      />
      <ul role="listbox" class="docs-command-palette__list">
        @for (command of filteredCommands(); track command.id) {
          <li role="option" [attr.aria-selected]="false">
            <button type="button" (click)="selectCommand(command.id)">
              {{ command.label }}
            </button>
          </li>
        } @empty {
          <li class="docs-command-palette__empty">
            {{ options()?.emptyText ?? 'No results' }}
          </li>
        }
      </ul>
    </div>
  `,
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomCommandPaletteComponent extends CommandPaletteBaseComponent {
  // The wrapper hands inputs to NgComponentOutlet by canonical name, so the
  // consumer's class arrives as `cssClass` rather than through the alias.
  override cssClass = input<string>('');

  containerClasses = computed(() =>
    ['docs-command-palette', this.cssClass()].filter(Boolean).join(' '),
  );

  onQueryChange(event: Event): void {
    this.query.set((event.target as HTMLInputElement).value);
  }
}

/**
 * Registering the implementation against
 * `COMMAND_PALETTE_STANDARD_COMPONENT_TOKEN` makes every
 * `<smart-command-palette>` in this injector render it instead of the standard
 * variation.
 */
@Component({
  selector: 'docs-command-palette-custom-example',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [CommandPaletteComponent],
  providers: [
    {
      provide: COMMAND_PALETTE_STANDARD_COMPONENT_TOKEN,
      useValue: CustomCommandPaletteComponent,
    },
  ],
  template: `
    <smart-command-palette
      [commands]="commands"
      [options]="options"
      [open]="true"
    />
  `,
})
export class CommandPaletteCustomExampleComponent {
  commands: ICommand[] = [
    { id: 'new-file', label: 'New file', group: 'Files' },
    { id: 'open-settings', label: 'Open settings', group: 'Files' },
    { id: 'toggle-theme', label: 'Toggle theme', group: 'View' },
  ];

  options: ICommandPaletteOptions = {
    placeholder: 'Search commands…',
    emptyText: 'No results',
  };
}

Preset

CommandPalettePresetComponent (<smart-command-palette-preset>) is the styled, production-ready replacement for the barebones standard component. It extends CommandPaletteStandardComponent, so all filtering (filteredCommands), query handling (onQueryChange), selection (selectCommand → emit runCommand + close), and close() behavior are inherited unchanged; the preset only adds the visual layer driven by options.variant.

The base look is a native <dialog [open]> styled smart:mx-auto smart:mt-16 smart:w-full smart:max-w-xl smart:rounded-xl with a gray border, white/dark:bg-gray-800 surface and smart:shadow-xl; a search input with a leading magnifier SVG (ps-11, focus:ring-blue-500); a <ul role="listbox"> with smart:divide-y rows that hover smart:bg-gray-100 dark:bg-gray-700; and a centered smart:text-gray-500 empty state using options.emptyText (fallback No results). All Tailwind utilities are smart:-prefixed with explicit dark: twins in the same template. Class recipes live in preset/preset-classes.util.ts (getCommandPalette*Classes, never barrel-exported).

Variants (options.variant, default simple)

VariantRendering
simpleBase dialog + listbox, opaque white/dark:gray-800 surface.
with-paddingLooser row padding (smart:px-6 smart:py-4) for a roomier list.
with-iconsLeading square glyph per row from ICommand.icon (falls back to # when absent).
with-imagesLeading rounded avatar per row from ICommand.imageUrl (rows without an image show no avatar).
semi-transparentTranslucent smart:bg-white/90 dark:bg-gray-800/90 smart:backdrop-blur dialog surface.
with-groupsGrouped rows under uppercase headers (data-role="group") from ICommand.group.
with-footerAdds a bottom bar (data-role="footer") with a result count and Enter / Esc hint text.
with-previewTwo-pane layout: half-width list + a preview panel showing the hovered (default first) command.

with-groups groups by ICommand.group; commands without a group fall under an Other header. with-preview uses ICommand.description for the preview body and widens the dialog to smart:max-w-3xl.

data-role hooks

The template exposes stable data-role attributes for testing/targeting: dialog, search-wrap, search, list, item, item-icon, item-image, group, empty, footer, preview-layout, preview.

Token registration

Because CommandPaletteComponent forwards inputs canonically through NgComponentOutlet (componentInputs = { commands, open, query, options, cssClass }), the preset declares override cssClass = input<string>('') (dropping the inherited class alias) so external classes bind.

Known limitation — model() two-way writeback

open and query are model() signals, but the wrapper renders the preset via NgComponentOutlet, which passes inputs by value and does not wire an output-side binding. So [(open)]/[(query)] two-way writeback from the injected preset does not propagate back up through the wrapper (a token-path limitation). The preset still updates its own open/query internally (e.g. selectCommand closes the dialog locally, onQueryChange filters), and runCommand is forwarded normally. If you need the parent's open/query signals kept in sync, use <smart-command-palette-preset> directly with [(open)]/[(query)] instead of going through the token, or drive open/query as one-way inputs and react to runCommand. This is a wrapper-level constraint and is intentionally not patched here.

Source

The component lives in packages/shared/angular/src/lib/components/command-palette and is documented for Claude Code by the angular-components-command-palette skill.