Skip to content
Ndop UI

Command

A search field over a list of commands, filtered as the user types.

Calendar
Search emoji
Calculator
Profile ⌘P
Billing ⌘B
Settings ⌘S

import { Component, signal } from '@angular/core';
import { NdIcon } from '@ndop/core';
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandGroupHeading,
  CommandInput,
  CommandInputWrapper,
  CommandItem,
  CommandList,
  CommandSeparator,
  CommandShortcut,
} from '@/ui/command/command';

/**
 * An inline command list with groups, shortcuts and a disabled command.
 * Focus stays in the search field; the arrow keys move the highlight and
 * Enter runs it.
 */
@Component({
  selector: 'example-command-demo',
  imports: [
    Command,
    CommandEmpty,
    CommandGroup,
    CommandGroupHeading,
    CommandInput,
    CommandInputWrapper,
    CommandItem,
    CommandList,
    CommandSeparator,
    CommandShortcut,
    NdIcon,
  ],
  template: `
    <div class="grid w-md max-w-full gap-2">
      <div ndCommand label="Commands" class="rounded-lg border shadow-md">
        <div ndCommandInputWrapper>
          <svg ndIcon="search"></svg>
          <input ndCommandInput placeholder="Type a command or search…" />
        </div>
        <div ndCommandList>
          <div ndCommandEmpty>No results found.</div>
          <div ndCommandGroup>
            <div ndCommandGroupHeading>Suggestions</div>
            <div ndCommandItem (triggered)="run('Calendar')">Calendar</div>
            <div
              ndCommandItem
              keywords="smiley emoji"
              (triggered)="run('Search emoji')"
            >
              Search emoji
            </div>
            <div ndCommandItem disabled>Calculator</div>
          </div>
          <div ndCommandSeparator></div>
          <div ndCommandGroup>
            <div ndCommandGroupHeading>Settings</div>
            <div ndCommandItem (triggered)="run('Profile')">
              Profile <span ndCommandShortcut>⌘P</span>
            </div>
            <div ndCommandItem (triggered)="run('Billing')">
              Billing <span ndCommandShortcut>⌘B</span>
            </div>
            <div ndCommandItem (triggered)="run('Settings')">
              Settings <span ndCommandShortcut>⌘S</span>
            </div>
          </div>
        </div>
      </div>
      <p class="text-sm text-muted-foreground" aria-live="polite">
        {{ ran() }}
      </p>
    </div>
  `,
})
export class CommandDemoExample {
  protected readonly ran = signal('');

  /**
   * Reports the command that ran.
   *
   * @param name - The command's name.
   */
  protected run(name: string): void {
    this.ran.set(`Ran ${name}.`);
  }
}

A command menu is a search field over a list of commands, filtered as the user types, such as a ⌘K palette. Focus stays in the field; the arrow keys move the highlight and Enter runs the highlighted command. Commands can be grouped, have keywords that match them too, and show their keyboard shortcuts. Put it in a Dialog to open it as a palette.

Installation

npx @ndop/cli add command

Usage

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandGroupHeading,
  CommandInput,
  CommandInputWrapper,
  CommandItem,
  CommandList,
  CommandSeparator,
  CommandShortcut,
} from '@/ui/command/command';
<div class="grid w-md max-w-full gap-2">
  <div ndCommand label="Commands" class="rounded-lg border shadow-md">
    <div ndCommandInputWrapper>
      <svg ndIcon="search"></svg>
      <input ndCommandInput placeholder="Type a command or search…" />
    </div>
    <div ndCommandList>
      <div ndCommandEmpty>No results found.</div>
      <div ndCommandGroup>
        <div ndCommandGroupHeading>Suggestions</div>
        <div ndCommandItem (triggered)="run('Calendar')">Calendar</div>
        <div
          ndCommandItem
          keywords="smiley emoji"
          (triggered)="run('Search emoji')"
        >
          Search emoji
        </div>
        <div ndCommandItem disabled>Calculator</div>
      </div>
      <div ndCommandSeparator></div>
      <div ndCommandGroup>
        <div ndCommandGroupHeading>Settings</div>
        <div ndCommandItem (triggered)="run('Profile')">
          Profile <span ndCommandShortcut>⌘P</span>
        </div>
        <div ndCommandItem (triggered)="run('Billing')">
          Billing <span ndCommandShortcut>⌘B</span>
        </div>
        <div ndCommandItem (triggered)="run('Settings')">
          Settings <span ndCommandShortcut>⌘S</span>
        </div>
      </div>
    </div>
  </div>
  <p class="text-sm text-muted-foreground" aria-live="polite">
    {{ ran() }}
  </p>
</div>

Examples

Command dialog

A command palette in a dialog, opened with Ctrl+K or Cmd+K.

import { Component, signal } from '@angular/core';
import { NdIcon } from '@ndop/core';
import { Button } from '@/ui/button/button';
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandGroupHeading,
  CommandInput,
  CommandInputWrapper,
  CommandItem,
  CommandList,
  CommandShortcut,
} from '@/ui/command/command';
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogOverlay,
  DialogPortal,
  DialogTitle,
  DialogTrigger,
} from '@/ui/dialog/dialog';

/**
 * A command palette in a dialog, opened with its button or with Ctrl+K
 * (⌘K on a Mac). Running a command closes it.
 */
@Component({
  selector: 'example-command-dialog',
  imports: [
    Button,
    Command,
    CommandEmpty,
    CommandGroup,
    CommandGroupHeading,
    CommandInput,
    CommandInputWrapper,
    CommandItem,
    CommandList,
    CommandShortcut,
    Dialog,
    DialogContent,
    DialogDescription,
    DialogHeader,
    DialogOverlay,
    DialogPortal,
    DialogTitle,
    DialogTrigger,
    NdIcon,
  ],
  host: { '(document:keydown)': 'shortcut($event)' },
  template: `
    <ng-container ndDialog [(open)]="open">
      <button ndButton variant="outline" ndDialogTrigger>
        Open palette <kbd class="text-muted-foreground text-xs">⌘K</kbd>
      </button>
      <ng-template ndDialogPortal>
        <div ndDialogOverlay></div>
        <div ndDialogContent class="overflow-hidden p-0">
          <div ndDialogHeader class="sr-only">
            <h2 ndDialogTitle>Command palette</h2>
            <p ndDialogDescription>Search for a command to run.</p>
          </div>
          <div
            ndCommand
            label="Commands"
            class="**:data-[slot=command-input-wrapper]:h-12 **:data-[slot=command-group-heading]:px-2 **:data-[slot=command-group-heading]:font-medium **:data-[slot=command-group-heading]:text-muted-foreground **:data-[slot=command-group]:px-2 **:data-[slot=command-input-wrapper]:[&>svg]:h-5 **:data-[slot=command-input-wrapper]:[&>svg]:w-5 **:data-[slot=command-input]:h-12 **:data-[slot=command-item]:px-2 **:data-[slot=command-item]:py-3 **:data-[slot=command-item]:[&_svg]:h-5 **:data-[slot=command-item]:[&_svg]:w-5"
          >
            <div ndCommandInputWrapper>
              <svg ndIcon="search"></svg>
              <input ndCommandInput placeholder="Type a command or search…" />
            </div>
            <div ndCommandList>
              <div ndCommandEmpty>No results found.</div>
              <div ndCommandGroup>
                <div ndCommandGroupHeading>Suggestions</div>
                <div ndCommandItem (triggered)="run('Calendar')">Calendar</div>
                <div ndCommandItem (triggered)="run('Search emoji')">
                  Search emoji
                </div>
              </div>
              <div ndCommandGroup>
                <div ndCommandGroupHeading>Settings</div>
                <div ndCommandItem (triggered)="run('Profile')">
                  Profile <span ndCommandShortcut>⌘P</span>
                </div>
                <div ndCommandItem (triggered)="run('Settings')">
                  Settings <span ndCommandShortcut>⌘S</span>
                </div>
              </div>
            </div>
          </div>
        </div>
      </ng-template>
    </ng-container>
    <p class="text-sm text-muted-foreground" aria-live="polite">{{ ran() }}</p>
  `,
})
export class CommandDialogExample {
  protected readonly open = signal(false);
  protected readonly ran = signal('');

  /**
   * Opens or closes the palette for Ctrl+K or ⌘K.
   *
   * @param event - A keydown anywhere in the document.
   */
  protected shortcut(event: KeyboardEvent): void {
    if (event.key.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey)) {
      event.preventDefault();
      this.open.update((open) => !open);
    }
  }

  /**
   * Runs a command and closes the palette.
   *
   * @param name - The command's name.
   */
  protected run(name: string): void {
    this.ran.set(`Ran ${name}.`);
    this.open.set(false);
  }
}

Accessibility

Implements the Combobox pattern of the WAI-ARIA Authoring Practices.

Keyboard

Keys the component handles.

Key Action
ArrowDownorArrowUpHighlights the next or previous shown, enabled item.
HomeorEndMoves the caret in the field, as in any text field.
EnterRuns the highlighted item.
aTypes into the field, filtering the list.

Focus

  • Focus stays in the field; aria-activedescendant points at the highlighted item, which scrolls into view.
  • Clicking an item runs it.

Right-to-left

Nothing mirrors: the list is vertical, and the field follows the text direction.

API reference

Command

[ndCommand]

A command menu (@ndop/primitives/command): a search field over a list of commands, filtered as the user types. Put it in a Dialog for a ⌘K palette.

Command binding Type Default
[class]
Extra classes, merged last so they win.
string''
[label]
Names the field and the list for assistive technology.
string | nullnull
[filter]
Tests whether an item matches what was typed.
NdFilterndContainsFilter
[shouldFilter]
Hides the items that don't match; false for lists filtered elsewhere.
booleantrue
[loop]
Makes the arrow keys wrap at the ends.
booleanfalse
[(search)]
The field's text. Two-way bindable with [(search)].
string''
Data attribute Values
data-slotcommand

CommandInputWrapper

[ndCommandInputWrapper]

The row around the search field and its icon.

CommandInputWrapper binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcommand-input-wrapper

CommandInput

input[ndCommandInput]role combobox

The search field; focus stays in it while the arrow keys move through the commands.

Manages aria-expanded, aria-controls, aria-activedescendant, aria-autocomplete, aria-label.

CommandInput binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcommand-input

CommandList

[ndCommandList]role listbox

The scrolling list of commands.

Manages aria-label, id.

CommandList binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcommand-list

CommandEmpty

[ndCommandEmpty]

What shows while no command matches.

Manages hidden.

CommandEmpty binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcommand-empty

CommandGroup

[ndCommandGroup]role group

A group of commands, named by its CommandGroupHeading.

Manages aria-labelledby, hidden.

CommandGroup binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcommand-group

CommandGroupHeading

[ndCommandGroupHeading]

The heading of a CommandGroup.

Manages id, aria-hidden.

CommandGroupHeading binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcommand-group-heading

CommandSeparator

[ndCommandSeparator]

A line between groups, hidden while searching.

Manages aria-hidden, hidden.

CommandSeparator binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcommand-separator

CommandItem

[ndCommandItem]role option

One command; triggered emits when it runs.

Manages aria-selected, aria-disabled, id, hidden.

CommandItem binding Type Default
[class]
Extra classes, merged last so they win.
string''
[value]
Identifies it; defaults to its text.
string | nullnull
[keywords]
Other words the search matches it by, separated by spaces.
string''
[disabled]
Makes it inert; the arrow keys skip it.
booleanfalse
Data attribute Values
data-slotcommand-item
data-selected —
data-disabled —

CommandShortcut

[ndCommandShortcut]

A command's keyboard shortcut, shown at its end. It's text for sighted users; the command doesn't bind it.

CommandShortcut binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotcommand-shortcut

Agent notes

Do

  • Name it with label.
  • Add keywords for the words people may search a command by.
  • Group commands and show an empty state.

Don't

  • Don't use it for picking a form value; use a Combobox.
  • Don't show shortcuts the app doesn't bind.