Searchable command launcher: a search field over grouped rows on the navy scrim, opened by the host on Ctrl or Cmd K. Typing filters by label and meta, the arrow keys walk the rows and Enter picks one. Reach for it when an app has more actions and records than a menu can hold.
import { Component } from "@angular/core";
import { SuiCommandPalette } from "@shineui/angular";
@Component({
standalone: true,
imports: [SuiCommandPalette],
template: `<sui-command-palette placeholder="Search actions, records, reports" brand="Falls City Books" [(open)]="open" [groups]="groups" (suiPick)="onChange($event)"></sui-command-palette>`,
})
export class Example {
open = false;
groups = [
{
heading: "Create",
items: [
{ label: "New invoice", icon: "fa-regular fa-file-lines", kbd: "N", meta: "Bill a customer" },
{ label: "Receive payment", icon: "fa-solid fa-circle-dollar-to-slot", kbd: "R", meta: "Apply to open invoices" },
{ label: "New journal entry", icon: "fa-solid fa-pen-nib", meta: "Post to the general ledger" },
],
},
{
heading: "Go to",
items: [
{ label: "Chart of accounts", icon: "fa-regular fa-rectangle-list", meta: "184 accounts" },
{ label: "Reconciliation", icon: "fa-solid fa-scale-balanced", meta: "Operating 4402" },
{ label: "Reports", icon: "fa-solid fa-chart-column", meta: "12 saved" },
],
},
/* ... 1 more */
]; // SuiCommandGroup[]
onChange(e: CustomEvent) { console.log(e.detail); }
}
Basic
Opening moves focus to the search field, a combobox that points at the active row with aria-activedescendant, and the result count is announced through a polite region. The palette does not bind a shortcut itself: the host listens for Ctrl or Cmd K and sets open.
query seeds the search field and is mutable, so typing writes it back. The built-in match reads the label and the meta line, and a group with no surviving rows drops out.
<sui-button id="cp-query-open" variant="outline" label="Open palette on a query"></sui-button>
<sui-command-palette id="cp-query" query="rec" placeholder="Search actions, records, reports"></sui-command-palette>
empty-filter-message replaces the row list and is the text the polite region announces when the query empties the palette. A palette holding no commands at all takes empty-message instead, which says Nothing here yet.
<sui-button id="cp-empty-open" variant="outline" label="Open palette with no matches"></sui-button>
<sui-command-palette id="cp-empty" query="payroll" empty-filter-message="No matches. Try an invoice number or a customer."></sui-command-palette>
external-filter renders groups as given, so answers the server matched loosely survive. Type "rcn" to see Reconciliation come back on a match no substring test would keep.
Idle.
<div style="display:flex;gap:14px;align-items:center;flex-wrap:wrap">
<sui-button id="cp-remote-open" variant="outline" label="Search the server"></sui-button>
<span id="cp-remote-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Idle.</span>
</div>
<sui-command-palette id="cp-remote" external-filter placeholder="Search the server" empty-message="The server found nothing."></sui-command-palette>
suiQueryChange fires on every keystroke with the new query, suiPick carries the chosen row, and suiClose carries the reason: pick, escape or scrim. Assigning query from code does not emit suiQueryChange, so a host can feed the field back without a loop.
Groups are { heading, items: [{ label, icon, kbd, meta }] }. suiPick carries the chosen item; the host owns open and hears a cancelable suiClose before the palette closes itself.
Keyboard
Key
Action
ArrowDown
Moves the active row down, across group headings, wrapping to the first.
ArrowUp
Moves the active row up, wrapping to the last.
Home
Moves the active row to the first row and scrolls it into view, as every move does.
End
Moves the active row to the last row and scrolls it into view.
Enter
Picks the active row: suiPick fires, then suiClose with reason pick.
Escape
Closes the palette with reason escape, when it is the topmost open modal.
API
<sui-command-palette>
Properties
Property
Attribute
Type
Default
Description
brand
brand
string
''
Small brand text in the footer, left of the key hints. Empty by default.
emptyFilterMessage
empty-filter-message
stringstring | undefined
Line shown when the query matched no command. An empty palette and a search that found nothing are different states and one sentence cannot be right about both.
emptyMessage
empty-message
stringstring | undefined
Line shown when there are no commands at all. Also the text the polite region announces.
externalFilter
external-filter
boolean
false
The host filters: groups renders as given, with no match test of its own, and only empty groups are dropped. Set it when the host answers suiQueryChange with the server's own answer for that query: a remote result is often a fuzzy or semantic match, and filtering it again on the client would drop rows the server returned. Off by default, so the palette filters groups by query, matching the label and the meta line.
groups
property only
SuiCommandGroup[]
[]
Groups: { heading, items: [{ label, icon, kbd, meta }] }[]. Property, not attribute.
open
open
boolean
false
Shows the palette as a fixed modal layer with the search field focused. Mutable; the element closes itself unless the close event is canceled. Only the topmost open modal answers Escape. The element never opens itself, so there is no suiOpen; watch open. While open, and while it fades out, the layer paints in the browser's top layer, so a transformed, clipping or stacked ancestor cannot trap or bury it; everything outside it is inert and the page does not scroll until it closes, and a node the page adds outside it while it is open goes inert too. The card is role="dialog" with no aria-modal, so VoiceOver keeps reading the toast region; Tab goes on from the palette into the toast stack and back. An element the page marks data-sui-keep-live stays usable with everything in it, and focus may move into it and back. On a short viewport the list gives up height so the search field and every row stay in reach.
placeholder
placeholder
stringstring | undefined
Search field placeholder.
query
query
string
''
Search text (the reference defaultQuery). Mutable; typing updates it.
Fires before the palette closes itself; detail is 'escape' | 'scrim' | 'pick'. Cancelable: preventDefault() keeps it open. It does not bubble.
suiPick
SuiCommandItem
onSuiPick
@sui-pick
Fires when a row is chosen (click or Enter); detail is the item. suiClose follows with reason 'pick'.
suiQueryChange
string
onSuiQueryChange
@sui-query-change
Fires on every keystroke in the search field; detail is the new query, so a host can run a remote or fuzzy search. Assigning query from code does not emit, so a host can feed the field back without a loop. Does not bubble.
Types
SuiCloseReasontypeWhy a dismissible surface is closing. Detail of the cancelable suiClose. A consumer expresses "a stray backdrop click must not close this" by calling preventDefault() on the reason it does not accept, which is why none of these needs a prop of its own.
No members.
SuiCommandGroupinterfaceA heading with its rows.
heading
string
items
SuiCommandItem[]
SuiCommandIteminterfaceOne launcher row. kbd is a single-key hint, meta a short secondary line.
label
string
icon?
string
kbd?
string
meta?
string
Theming
Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.