On this page
Import
import { CommandPalette } from '@envsemble/envui';
Usage
The application owns the items and what choosing one does.
openopens the palette, such as from a shortcut and from a visible control.onOpenChangereceives false when Esc, a press on the backdrop or a choice closes it, and why, as a dialog's does:escape-key,outside-press, oritem-pressfor a choice.- The application filters and ranks the
itemsitself, from the text ininputValueandonInputValueChange, and clears that text as the palette closes. onActionruns the chosen item, andcloseOnAction, true by default, closes the palette after it.aria-labelnames the dialog, its search field and its list.placeholdershows in the empty field.suggestedItemsshow in place ofitemswhile the field is empty, and the empty state in place of the list when there are no options.
import { useMemo, useState } from 'react';
import { Button, CommandPalette, Column, Text, type CommandPaletteItem } from '@envsemble/envui';
import { IconDescription } from '@envsemble/envui/icons/Description';
import { IconSearch } from '@envsemble/envui/icons/Search';
const documents = [
{ name: 'Roadmap.md', folder: 'Plans', recent: true },
{ name: 'Pricing research.md', folder: 'Research', recent: true },
{ name: 'Launch checklist.md', folder: 'Plans/Northwind', recent: true },
{ name: 'Interview notes.md', folder: 'Research/Interviews', recent: false },
{ name: 'Support handbook.md', folder: 'Guides', recent: false },
{ name: 'Quarterly review.md', folder: 'Reports', recent: false },
];
export default function GoToDocument() {
const [open, setOpen] = useState(false);
const [query, setQuery] = useState('');
const [opened, setOpened] = useState('');
// The application filters and ranks the items; here, every word of the query must match.
const items = useMemo<CommandPaletteItem[]>(() => {
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
return documents
.filter((document) =>
words.every((word) => `${document.name} ${document.folder}`.toLowerCase().includes(word)),
)
.map((document) => ({
id: `${document.folder}/${document.name}`,
label: document.name,
description: document.folder,
icon: <IconDescription />,
group: !query && document.recent ? 'Recently opened' : 'Documents',
}));
}, [query]);
return (
<Column gap={3} align="center">
<Button variant="secondary" startIcon={<IconSearch />} onClick={() => setOpen(true)}>
Search
</Button>
<CommandPalette
open={open}
onOpenChange={(next) => {
setOpen(next);
if (!next) setQuery('');
}}
aria-label="Go to document"
placeholder="Search documents by name or folder"
inputValue={query}
onInputValueChange={setQuery}
items={items}
onAction={(item, { modifier }) =>
setOpened(`${modifier ? 'Opened to the side' : 'Opened'}: ${item.id}`)
}
emptyLabel="No matching documents"
hints={[
{ keys: ['Enter'], label: 'Open' },
{ keys: ['Ctrl', 'Enter'], label: 'Open to the side' },
{ keys: ['Esc'], label: 'Close' },
]}
/>
<Text variant="caption" emphasis="muted" role="status">
{opened}
</Text>
</Column>
);
}Items
Each CommandPaletteItem has an id and a label, with textValue for a label that isn't plain text.
descriptionis muted text under the label, such as a folder.iconis a decorative icon before it.endis content at the end, such as a Kbd shortcut or a Badge.disabledshows an item that can't be chosen.groupnames its group, whose heading shows where the group changes between items, such as Recently opened while the query is empty.
Suggestions and the empty state
suggestedItems show while the field is empty, or holds only spaces, in place of items, such as recent documents or suggested pages with a group heading; they are options like any other, highlighted, chosen with Enter or Ctrl+Enter and passed to onAction.
With no options, the empty state shows in place of the list as the polite status: one muted line at an option's height, its title and then its description after a dot, so the palette keeps about its height as results come and go.
- While the field is empty: Type to search, with no description;
emptyDescriptionsays what the palette finds. - When the text finds nothing: No results, and a hint that quotes the trimmed text.
- While results load: the loading label, with a spinner at the search field's end, which also shows while results already found stay.
emptyLabelandemptyDescriptionreplace the title and the description in either state,nullorfalsehiding the description, andemptyIconputs an icon at the start of the line, where an option's sits, such as an error icon. Pass a value that depends on the query to change one state only.- The icon is decorative and hidden from assistive technology; the title and description are announced.
const typed = query.trim() !== '';
<CommandPalette
aria-label="Go to document"
inputValue={query}
onInputValueChange={setQuery}
items={matches}
// Recent documents, each with group: 'Recently opened'.
suggestedItems={recent}
emptyLabel={typed ? 'No matching documents' : undefined}
emptyDescription={typed ? undefined : 'Find a document by its name or folder.'}
{...props}
/>;
Commands and key hints
onAction receives the item and { modifier }, true while Ctrl or Cmd was held, such as to open beside. hints show keys below the list as Kbd, such as { keys: ['Enter'], label: 'Open' }, and footer holds other content after them, as the RunCommand example does. A hint with Enter in its keys hides while there is no option to choose, as results load or when nothing matches.
<CommandPalette
open={open}
onOpenChange={setOpen}
aria-label="Run a command"
inputValue={query}
onInputValueChange={setQuery}
items={[
{ id: 'new', label: 'New document', icon: <IconNoteAdd />, end: <Kbd keys={['Ctrl', 'N']} /> },
{ id: 'automations', label: 'Create an automation', end: <Badge tone="primary">New</Badge> },
]}
onAction={(item, { modifier }) => run(item.id, { beside: modifier })}
hints={[{ keys: ['Enter'], label: 'Run' }]}
footer="Shortcuts work anywhere in the workspace."
/>
The button that opens it
The button is a Button with variant="secondary", IconSearch as its startIcon, and a Kbd of the keys in its end slot, as the InSidebar example does.
aria-hiddenon the Kbd andaria-keyshortcutson the button announce the shortcut once.- Its
onClickopens the palette. - With
fullWidthit fills its container, such as a NavigationDrawer'sheader.
<Button
variant="secondary"
fullWidth
startIcon={<IconSearch />}
end={<Kbd keys={[modifier.label, 'K']} aria-hidden />}
aria-keyshortcuts={`${modifier.key}+K`}
onClick={() => setOpen(true)}
>
Search
</Button>
The shortcut
The application listens for the shortcut once, at its root, however many buttons show it, and opens the same palette the button does. It skips an event whose defaultPrevented is set, so a key press that something else already handled, such as an editor's own shortcut, is left alone.
useModifierKey(), on Kbd's page, gives the platform's modifier.
- Its
key,MetaorControl, foraria-keyshortcutsand the listener. - Its
label, ⌘ or Ctrl, for Kbd. - It is Ctrl on the server and while the page hydrates, so server-rendered markup matches, and becomes ⌘ on Apple platforms right after.
- Don't read
navigatorwhile rendering: it doesn't exist on the server, and markup that differs from the server's fails to hydrate.
const modifier = useModifierKey();
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
const pressed =
modifier.key === 'Meta' ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey;
if (event.defaultPrevented || !pressed || event.altKey || event.shiftKey) return;
if (event.key.toLowerCase() !== 'k') return;
event.preventDefault();
setSearchOpen((open) => !open);
};
addEventListener('keydown', onKeyDown);
return () => removeEventListener('keydown', onKeyDown);
}, [modifier.key]);
Icon only
In a narrow header, the button is an IconButton with variant="ghost", aria-label="Search" and the same aria-keyshortcuts, inside a Tooltip with side="bottom" whose content holds the label and the Kbd, as the IconOnly example does.
<Tooltip
side="bottom"
content={
<Row render={<span />} align="center" gap={2}>
Search <Kbd keys={[modifier.label, 'K']} />
</Row>
}
>
<IconButton
variant="ghost"
icon={<IconSearch />}
aria-label="Search"
aria-keyshortcuts={`${modifier.key}+K`}
onClick={() => setOpen(true)}
/>
</Tooltip>
Composition
The palette is env-command-palette: its search field, env-command-palette-search, a large ghost TextField kept solid on the frosted palette, with its input, env-command-palette-input, and the close button shown under a coarse pointer, env-command-palette-close, in env-command-palette-header, the group headings, env-command-palette-group-label, the items, env-command-palette-item, the key hints in env-command-palette-footer, and the empty state, env-command-palette-status with data-empty: its icon, env-command-palette-empty-icon, title, env-command-palette-empty-title, and description, env-command-palette-empty-description. className, style, ref and data attributes reach the surface.
The button that opens it is Button's: env-button, its icon, env-button > env-icon, its label, and the keys in its end slot, env-button-end. The icon-only button is an env-icon-button in a tooltip.
Customize
Its popup is frosted, as every popup is: --env-popup-opacity, --env-popup-blur and --env-popup-shadow, set on a theme, change it, as Popover describes.
Words in code
| Words | In code |
|---|---|
| Suggestions | suggestedItems, with a group such as Suggested |
| Recent items | suggestedItems, with a group such as Recently opened |
| Type to search | The empty state with an empty query: emptyDescription |
| No results | emptyLabel, emptyDescription and emptyHint |
| With an icon | emptyIcon |
Accessibility
You provide
- The shortcut that opens the palette, and a visible control that opens it too.
aria-keyshortcutson that control for the platform,`${modifier.key}+K`fromuseModifierKey(), andaria-hiddenon the keys it shows, so the shortcut is announced once.- An
aria-label, and the items, filtered and ranked. textValuefor labels that aren't plain text.
envUI provides
- A modal dialog named by
aria-label, with focus in its search field, kept inside and returned on close. - Under a coarse pointer, such as a phone's, a close button after the search field in place of Esc, and no keys: neither the items' shortcuts nor the key hints show.
- The field as a
comboboxover alistbox, with the highlighted option as its active descendant. - A polite count of results as they change.
- The empty state's title and description in that same polite status, with its icon hidden from assistive technology.
- For the button that opens it, a native
buttonnamed by its label, or byaria-labelwhen it shows only its icon, and the keys as Kbd, in the button's content color on every variant.
Props
CommandPalette
A quick-open dialog near the top of the window: a search field over a list of options the application filters and ranks, built on Base UI's Autocomplete inside a dialog. The arrow keys move the highlight while typing continues, and the number of results is announced as it changes. className, style, ref and data attributes go to the dialog's surface.
| Prop | Type | Description |
|---|---|---|
openrequired | boolean | Whether the palette is open; the application opens it, such as from a shortcut. |
onOpenChangerequired | ( | Called with false, and why, when Esc, a press on the backdrop or a choice closes the palette. |
aria-labelrequired | string | Accessible name of the dialog, the search field and the list. |
inputValuerequired | string | The text in the search field. |
onInputValueChangerequired | ( | Receives the text as people type. |
itemsrequired | readonly CommandPaletteItem[ | Filtered and ranked by the application, in display order. |
suggestedItems | readonly CommandPaletteItem[ | Shown in place of items while the search field is empty, such as recent documents or suggested pages under a group heading; options like any other. |
onActionrequired | ( | Enter or a press on an option; Ctrl or Cmd sets modifier. |
closeOnActionDefault true | boolean | Close after an option is chosen. |
placeholder | string | Shown in the empty search field, such as "Search documents by name or folder". |
loadingDefault false | boolean | The results are on their way, such as from a server. A spinner shows at the search field's end; with no results to show yet, the empty state's line says loadingLabel, and results already shown stay, marked busy. |
loadingLabelDefault 'Searching…', or EnvUIProvider's messages.commandPalette.loading | ReactNode | Shown and announced while loading. |
emptyLabelDefault 'Type to search' while the search field is empty and 'No results' when its text finds nothing, or EnvUIProvider's messages.commandPalette.emptyQuery and empty | ReactNode | The empty state's title, the start of the one muted line shown in place of the list when there are no options, and announced. |
emptyDescriptionDefault None while the search field is empty; when its text finds nothing, 'Nothing matches “…”. Check the spelling or try other words.', or messages.commandPalette.emptyHint | ReactNode | Text after the empty state's title on its line, such as what the palette searches; null or false shows none. |
emptyIcon | ReactNode | A decorative icon at the start of the empty state's line, in an option's icon place, such as an error icon when the search failed; none by default. |
hints | readonly CommandPaletteHint[ | Key hints below the list, such as { keys: ['Enter'], label: 'Open' }. A hint with Enter in its keys hides while there is no option to choose. |
footer | ReactNode | Other content below the list, after the hints. |
Also accepts the native props of <div>, such as ref, className and aria-* attributes.
Part names
Each part's class and state attributes are public and versioned, for an application's CSS to style.
env-command-palette- The command palette's dialog.
env-command-palette-close- The close button after the search, shown under a coarse pointer.
env-command-palette-empty-description- The empty state's text after its title, on its line.
env-command-palette-empty-icon- The empty state's icon, at the start of its line.
env-command-palette-empty-title- The empty state's title.
env-command-palette-footer- The footer with the keyboard hints.
env-command-palette-footer-content- The content of the footer.
env-command-palette-group- A group of commands.
env-command-palette-group-label- A group's label.
env-command-palette-header- The row of the search field and the close button.
env-command-palette-hint- One keyboard hint.
env-command-palette-input- The search input.
env-command-palette-item- One command.
env-command-palette-list- The list of commands.
env-command-palette-positioner- The palette's positioner.
env-command-palette-search- The search field, a ghost TextField around the search input.
env-command-palette-status- The empty state in place of the list, and the announcement of the number of results.
data-empty- Present on env-command-palette-status
data-ending-style- Any value, or present
data-starting-style- Any value, or present
Strings
EnvUIProvider's messages overrides each, and a component's own label prop wins over it.
messages.commandPalette.emptyQuery"Type to search"The empty state's title while the search field is empty and nothing shows in the list.messages.commandPalette.empty"No results"The empty state's title when the text in the search field finds nothing.messages.commandPalette.emptyHint({ query }) => stringUnder that title: what found nothing, and what to try, such as Nothing matches “roadmap”.messages.commandPalette.loading"Searching…"The empty state's title while results are on their way, and what is announced meanwhile.messages.commandPalette.results({ count, countText }) => stringAnnounced as the results change, such as 3 results; `countText` is the count in the locale's digits.
Changelog
- 1.0.0
Opens from a Button with the shortcut's keys in its new
endslot, in place of the SearchTrigger component. Rebuilt on Base UI's autocomplete:inputValue,onInputValueChange,onActionandcloseOnActionforquery,onQueryChange,onSelectandcloseOnSelect, andCommandPaletteActionDetails; items takedescription,endanddisabled; options are list rows; the number of results is announced;className,style,refand data attributes reach the surface. A group's label,env-command-palette-heading, isenv-command-palette-group-label, as in Menu.- 1.0.0-prerelease.6
The palette fades in with
--env-motion-ease-enter, so it shows from its first frames rather than late, while it drops into place as before.An item's shortcut is muted text in one unit, such as Ctrl+N, rather than a filled chip for each key; the key hints below the list keep their keys.
The empty state is one muted line at an option's height, its title and description after a dot, rather than an icon on a circle over a centred title, so the palette keeps about its height as results come and go; it shows an icon only when
emptyIcongives one. While results load, a spinner turns at the search field's end, rather than in place of its icon or the empty state's.While it holds the caret, the search's edge takes the focus color as a hairline, rather than a ring that read as a second frame.
A key hint for Enter hides while there is no option to choose, as results load or when nothing matches.
Under a coarse pointer the palette hides the keys, the shortcuts in its items and the key hints of its footer, and shows a close button after the search field,
env-command-palette-close, for touch screens with no Esc; the two share a row,env-command-palette-header.- 1.0.0-prerelease.5
onOpenChangereceives why the palette closes,CommandPaletteOpenChangeDetails, as a dialog's does.defaultItemsissuggestedItems:defaultnames an uncontrolled starting value everywhere else.The palette is frosted: the popup's color lets a little of what is behind through, blurred, with a soft shadow and no outline edge but a faint hairline in dark, and is solid with its edge at high contrast and with reduced transparency. Its search is a large ghost TextField, opaque on the neutral fill with no outline, whose edge returns at high contrast; its headings are small and muted, its rows and keys are the popup lists', the keys side by side with the + read but not shown, and its hints are in the small label size. Its options take the text color rather than the controls', lie one under the next with no space between them, and its headings sit a little ahead of the rows.
- 1.0.0-prerelease.4
An empty state with an icon, a title and a description, which tells an invitation before people type from no results;
emptyDescription,emptyIconandmessages.commandPalette.emptyQueryandemptyHint.defaultItemsshow before people type.- 1.0.0-prerelease.3
The search trigger pattern is part of this page, as the button and the shortcut that open the palette; the pattern's address opens it.