GrCommandPalette
Opens a ⌘K search over app commands with groups and shortcuts.
Overview
When to take it
- there are many commands and they are scattered across the interface — the palette gives one way in instead of a search through the menus;
- the user works with the keyboard —
hotkeyopens the palette from anywhere, and everything after that is done with the arrows; - the commands are grouped — sections, recents (
recentIds) and shortcut hints are already there; - the source is asynchronous —
sourceloads items on request,virtualholds a long list.
When to take something else
| Need | Take |
|---|---|
| A form field value is being selected | GrSelect |
| An object is searched for with loading | GrAutocomplete |
| The actions belong to a single object | GrDropdownMenu |
| A catalogue of dashboard widgets | GrDashboardPalette |
| A search across the content of a page rather than commands | GrInput |
The palette does not replace navigation: it speeds up the work of someone who already knows what they are looking for. It cannot be the only way to reach a section — a new user will not guess to press a shortcut.
Playground controls come from the API, not from hand
This component lives above the screen, not inside a canvas: a screen opens it, and the playground has nothing to open it from. The examples below show it at work.
0 changed· state is in the URL
<GrCommandPalette>Label</GrCommandPalette>Examples 3 each with a live demo and its code
The command bar of an app
Open it with the trigger or ⌘J —
hotkeyhangs a global shortcut, and the trigger says so. The list is flat:groupmakes the sections,shortcutdraws the keys,descriptiongives the context,keywordslet «vat» find Billing settings, and adisabledcommand stays visible with its reason.recent-idsbring the last commands back on top; the page keeps the history.Acme BillingINV-0418 · Northwind Traders€1,284.50 · due 30 Sep · 6 days overdueCustomers searched on the server
Twelve thousand customers do not fit in the browser.
filterable="false"hands filtering to the owner: the query comes out withsearch,loadingshows the wait, and the palette renders what came back. Theitemslot draws a row of its own, and theemptyslot turns «nothing found» into «create it».Customers12,408 accounts · searched on the serverTry «north» or «contoso».A workspace switcher with its own filter
A custom
filterfinds «wwi» by initials as well as by any part of the name.recent-idskeep the last two workspaces on top while the query is empty,size="sm"makes the palette compact, and thefooterslot keeps the keys where the hands are.WorkspaceNorthwind Traders
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
filter | GrCommandFilter | undefined | undefined | A matcher of your own for the local filtering. |
size | "sm" | "md" | "lg" | "xl" | "full" | undefined | undefined | The width of the window on the `GrModal` panel scale. `lg` by default; taken from `GrConfigProvider`. |
placeholder | string | undefined | undefined | A hint in the search field. By default — a locale string. |
ariaLabel | string | undefined | undefined | The accessible name of the palette and the search field. By default — a locale string. |
loading | boolean | undefined | false | The loading state governed from the outside (for a remote search). |
filterable | boolean | undefined | true | Local filtering by the query. `false` — the owner filters on the `search` event. |
closeOnSelect | boolean | undefined | true | Close the palette after a command is chosen. |
virtual | boolean | undefined | false | Virtualisation of the list: only the window around the viewport lives in the DOM. The height of the window is set by `maxHeight`. Switch it on deliberately: on a hundred commands there is no gain, while only the window is left in the markup — and with it changes what the consumer’s `querySelector` finds. The scenario it is meant for is an application palette of thousands of commands. |
items | GrCommandItem[] | undefined | undefined | A flat list of commands; the grouping comes from the `group` field of a command itself. |
emptyText | string | undefined | undefined | The text of an empty result. The `#empty` slot is stronger; by default — a locale string. |
recentIds | string[] | undefined | undefined | The recent commands: while the query is empty they are raised to the top as a group of their own in this order and are not duplicated below. |
hotkey | string | null | undefined | "mod+k" | The global combination that opens it. `null` — do not hang a listener. |
hotkeyId | string | undefined | undefined | The identifier of an entry in the registry (`granularityHotkeysPlugin`) instead of the combination as a string: the hint and the binding are taken from one source and cannot diverge. Stronger than `hotkey`. |
maxHeight | number | undefined | 360 | The maximum height of the list, in px. |
showHotkeyHint | boolean | undefined | true | Whether to show the hint of the combination in the input field. |
modelValuerequiredv-model | boolean | — | Whether the palette is open. |
Slots
| Slot | Type | Description |
|---|---|---|
item | { item: GrCommandItem; active: boolean; } | A row of the list instead of the standard one. |
empty | { query: string; } | The empty state: it receives the query, so that an action can be offered for it. |
footer | any | The footer of the palette: key hints, a counter. |
Events
| Event | Signature | Description |
|---|---|---|
update:modelValue | (event: "update:modelValue", value: boolean): void | — |
search | (event: "search", query: string): void | — |
select | (event: "select", item: GrCommandItem): void | — |
Methods
| Methods | Signature | Description |
|---|---|---|
open | () => void | Open the palette — for a button in the header or a menu item next to the shortcut. |
close | () => void | Close the palette. |
toggle | () => void | Toggle the palette — the same as the global shortcut. |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrCommandItem, GrCommandGroup, GrCommandFilter } from '@feugene/granularity/components/GrCommandPalette'Filedist/types/components/GrCommandPalette/config.d.ts
const grCommandPaletteConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrCommandPalette">;Filedist/types/components/GrCommandPalette/filtering.d.ts
type GrCommandItem = {
id: string;
label: string;
description?: string;
icon?: string | Component;
shortcut?: string[];
group?: string;
keywords?: string[];
disabled?: boolean;
};type GrCommandGroup = {
name: string | undefined;
items: GrCommandItem[];
};type GrCommandFilter = (item: GrCommandItem, query: string) => boolean;
export declare function matchCommandItem(item: GrCommandItem, query: string): boolean;
export declare function filterCommandItems(items: GrCommandItem[], query: string, filter?: GrCommandFilter): GrCommandItem[];
export declare function groupCommandItems(items: GrCommandItem[]): GrCommandGroup[];
export type GrCommandMatchSegment = {Filedist/types/components/GrCommandPalette/GrCommandPalette.vue.d.ts
interface GrCommandPaletteProps {
modelValue: boolean;
items?: GrCommandItem[];
recentIds?: string[];
placeholder?: string;
size?: GrCommandPaletteSize;
hotkey?: string | null;
hotkeyId?: string;
filterable?: boolean;
filter?: GrCommandFilter;
loading?: boolean;
closeOnSelect?: boolean;
maxHeight?: number;
virtual?: boolean;
showHotkeyHint?: boolean;
emptyText?: string;
ariaLabel?: string;
}interface GrCommandPaletteEmits {
(e: 'update:modelValue', value: boolean): void;
(e: 'select', item: GrCommandItem): void;
(e: 'search', query: string): void;
}Filedist/types/components/GrCommandPalette/grCommandPaletteStyles.d.ts
type GrCommandPaletteSize = GrOverlaySize;Filedist/types/components/GrCommandPalette/safelist.d.ts
const grCommandPaletteSafelist: string[];Filedist/types/components/GrCommandPalette/index.d.ts
type GrCommandPaletteInstance = ComponentExposed<typeof GrCommandPaletteComponent>;Accessibility
APG pattern
dialog + listbox
Keys
mod+K↑ / ↓Home / EndEnter
Full contract of the package: docs/keyboard.md ↗
Usage from the library prose
The commands
The model is flat: id, label, the optional description, icon, shortcut,
group, keywords, disabled. icon is a Vue component or an icon class
(i-lucide-* — the application sets up the rule for it: in the engine
factory or through a provider, otherwise a Vue component; see “Icons”). The groups are assembled
from group in the order of first appearance;
commands without a group stay a nameless group in their place.
id is required and has to be unique: it is also the render key and the target of
aria-activedescendant. Duplicates give identical DOM ids, and the focus moves to
the wrong command — in a dev build the component warns about that.
The filter and the highlighting
By default a substring (case-insensitive) is matched in the label, the description,
the name of the group and keywords. The matched fragment of the label and the
description is highlighted with <mark>; the colour is set by the
--gr-command-match-bg variable.
A filter of your own may match by anything at all — for instance by keywords
only. Then there is no match in the label and no highlighting appears: there is
nothing to highlight.
filterable="false" hands the filtering outside (a remote search): the component
shows what has arrived and gives the query away with a search event.
The recents
recentIds raises commands to the top as a group of their own — in the order of the
array itself, not of the list of commands. They are removed from the rest of the
list: one command cannot occur twice, it has a single id.
The section lives only while the query is empty. With a non-empty query relevance rules: history would draw the eye to the wrong place from the very first letter.
The states
Loading and “nothing found” are shown by a single live region (role="status",
aria-live="polite") outside the list. The spinner icon is decorative in the
process: an aria-label on an element without a role is ignored by most AT, and
before that the loading state was not announced at all.
An empty list takes up no space: the message sits right below the field, and the
role="listbox" itself stays in the DOM — the field’s aria-controls points to it. To
keep the window from collapsing between “searching” and “found”, reserve the height of
the results area, not of the list:
[data-gr-command-palette] {
--gr-command-results-min-height: 14rem;
}Then “Nothing found”, the loading state and the #empty slot sit in the centre of the
reserve. A reserve on the list itself (min-height on [data-gr-command-palette-list])
would push the message below an empty strip.
Inside role="listbox" the only direct children remain role="group" — the heading
of a group lies inside it and is declared presentational, and it gives the group its
name through aria-labelledby. Otherwise aria-required-children breaks, and the
panel here is always expanded, that is, this is the main state.
Virtualisation
virtual keeps in the DOM only the window around the viewport; the height of the
window is set by maxHeight. The scenario it is meant for is an application palette
with thousands of commands.
<GrCommandPalette v-model="open" :items="commands" virtual :max-height="360" />The groups survive the window. If the list is scrolled into the middle of a
group, its heading is no longer in the markup — the role="group" wrapper is created
all the same and takes its name through aria-label instead of aria-labelledby.
The set is counted per group. With virtual the commands carry
aria-setsize/aria-posinset, and that is the size of their group rather than of
the whole palette — that is what ARIA requires. In the ordinary mode the attributes
are absent: there the set is visible in the DOM.
The active command is always mounted. The arrows scroll the list to it before
moving aria-activedescendant. How the primitive works —
virtual-list.md.
The keyboard and the hotkey
↑/↓ walk the commands (disabled ones are skipped, the walk is a ring),
Home/End go to the edges, Enter runs, Esc closes. The order of the walk
matches the order on the screen, the recents included.
hotkey (mod+k by default) hangs a global shortcut; mod is ⌘ on Apple and Ctrl
everywhere else. The platform is detected after mounting: there is no navigator
on the server, and the first client render has to match the server one, otherwise the
hint shifts during hydration.
The imperative API
open(), close() and toggle() through a ref on the component — the same set as
in the other overlays. They are needed where the palette is opened by something other
than its hotkey: a “Find a command” menu item, a button in the header, a link from
the onboarding.
The palette is controlled, so the methods emit update:modelValue — the state stays
in the parent’s v-model (more on that in GrModal.md).
Props in this example