Command
A list of actions that filters as you type and runs from the keyboard, in a dialog or on the page.
Command is a list of actions you search by typing. It filters and ranks the rows as you
type, moves a highlight with the arrow keys, and runs the highlighted row on Enter, so every
action is a few keys away. CommandDialog puts it in a Dialog
for a ⌘K menu, and a bare Command sits on the page. It is built on cmdk, and
Combobox and
Command palette are built on it.
In an app, bind it to ⌘K. The docs search already has that key here, so the button opens it.
Nothing run yet.
Usage
import { Plus } from '@phosphor-icons/react';
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command';
<CommandDialog open={open} onOpenChange={setOpen} title="Run a command">
<CommandInput placeholder="Type a command…" />
<CommandList>
<CommandEmpty>No results.</CommandEmpty>
<CommandGroup heading="Create">
<CommandItem
onSelect={() => {
setOpen(false);
createIssue();
}}
>
<Plus />
Create issue
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>The file exports Command, CommandDialog, CommandInput, CommandList, CommandEmpty,
CommandGroup, CommandItem, CommandSeparator, and CommandShortcut. Icons in a row are
drawn at 16px, the highlighted row takes the bg-fill wash, and a disabled row dims and
ignores the pointer.
Opening it with ⌘K
CommandDialog is controlled like Dialog, so hold open in state and flip it from a key
listener. Running a row does not close the dialog, so call setOpen(false) in its
onSelect, as the snippet above does.
import * as React from 'react';
React.useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
setOpen((open) => !open);
}
}
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, []);Escape and a click outside close it, so it has no close button unless you set
showCloseButton, and focus goes back to whatever had it when the dialog opened. Its title
names the dialog and its field, and its description describes the dialog. Neither is shown,
and they default to "Command palette" and "Search for a command to run…", so set them to say
what this one is for.
Filtering as you type
cmdk scores every row against what is typed, hides the rows that do not match, and sorts the
rest best first, and a group with no matching row hides with its heading. Separators hide
while there is a search, unless you set alwaysRender, and CommandEmpty shows when nothing
matches. Try "prog", "urg", or "p2" below.
Nothing picked yet.
A row's value is what it is matched on, and when you leave it out it is read from the
row's text, a CommandShortcut included. Set value when the row holds more than its name,
and add keywords for the other words someone might type, which is how "p2" finds High
above. value also names the row for cmdk, so keep it unique.
When the rows come from somewhere else, like a server, set shouldFilter={false} on
Command and render only the rows you want, with the field controlled through
CommandInput's value and onValueChange. filter swaps in your own scoring and keeps
the rest. Command palette is built this way and
shows a server search.
Without the dialog
A bare Command renders on the page, in a panel or a popover, and the demo above is one.
Pass it a label, which names the field for assistive tech. It is a rounded
bg-surface-raised box with no border, so add one when it sits on a surface of the same color.
CommandDialog hands its props to Dialog and not to cmdk, so loop, shouldFilter, and the
other list props cannot reach the list from there. When you need them in a dialog, put a
Command inside the kit Dialog yourself, as Command palette does.
Shortcuts and slots
CommandShortcut sets a key hint at the end of a row in muted text. Put a
Kbd in it so the hint reads as a key. It only shows the key,
and binding it is yours.
CommandInput takes leading for something after the search glyph, like the name of a list
you have opened, trailing for something after the field, like a key hint, and
wrapperClassName for the row that holds them. Command palette puts the open list's badge
in leading and its esc hint in trailing.
import { Plus } from '@phosphor-icons/react';
import { CommandInput, CommandItem, CommandShortcut } from '@/components/ui/command';
import { Kbd } from '@/components/ui/kbd';
<CommandInput placeholder="Type a command…" trailing={<Kbd>esc</Kbd>} />
<CommandItem value="Create issue" onSelect={() => createIssue()}>
<Plus />
Create issue
<CommandShortcut>
<Kbd>C</Kbd>
</CommandShortcut>
</CommandItem>Props
CommandDialog takes Dialog's root props, open, defaultOpen, onOpenChange, and
modal, plus these.
| Prop | Type | Default | What it does |
|---|---|---|---|
title | string | "Command palette" | Names the dialog and its field for assistive tech. It is not shown. |
description | string | "Search for a command to run…" | Describes the dialog for assistive tech. It is not shown. |
showCloseButton | boolean | false | Adds Dialog's close button in the corner. |
className | string | Goes on the dialog panel, so sm:max-w-2xl widens it. |
CommandInput takes cmdk's input props, including a controlled value and
onValueChange, plus these.
| Prop | Type | Default | What it does |
|---|---|---|---|
leading | ReactNode | Drawn after the search glyph, like the name of a list you have opened. | |
trailing | ReactNode | Drawn after the field, like a key hint. | |
wrapperClassName | string | Classes for the row around the glyph, the field, and the slots. className goes on the field. |
The rest take cmdk's props as they are. On Command, label names the field, loop wraps
the highlight at the ends, shouldFilter and filter replace the search, value and
onValueChange control the highlighted row, disablePointerSelection keeps the pointer from
moving the highlight, and vimBindings={false} turns off the Ctrl keys. On CommandItem,
value, keywords, disabled, and onSelect, which gets the row's value.
Accessibility
- The field's placeholder is in
text-fg-muted, the stronger ink, because a command field usually has no visible label and the placeholder is the one instruction it shows. - The field is a
comboboxthat controls alistboxofoptionrows, and it points at the highlighted row witharia-activedescendant, so focus stays in the field while the highlight moves and a screen reader reads each row as it is reached. - ↑ and ↓ move the highlight, Home and End or ⌘↑ and ⌘↓ jump to the first and last row,
Alt ↑ and Alt ↓ jump between groups, and Enter runs the highlighted row. Ctrl N or J and
Ctrl P or K move it too, unless
vimBindingsis off. The highlight stops at either end unlessloopis set, and the keys wait while an input method is composing. - Opening highlights the first row, and the list scrolls to keep the highlighted row in view. Moving the pointer over a row highlights it, and a click runs it.
- A group's rows are a
groupnamed by its heading, a separator is aseparator, and adisabledrow isaria-disabledand skipped by the arrow keys. - cmdk names the field with a hidden label that holds
Command'slabel.CommandDialogfills it withtitle, and a bareCommandneeds alabel. The label wins over anaria-labelonCommandInput, which only counts whenlabelis empty. CommandDialognames the dialog withtitleand describes it withdescription. On open, focus moves to the field and stays in the dialog, the page behind is hidden from assistive tech and does not scroll, and Escape or a click outside closes it.- Closing returns focus to whatever had it when the dialog opened, like the button that opened it, even though the dialog opens from your state rather than a Dialog trigger.
CommandEmptyis not a live region, so a screen reader is not told when nothing matches.- Under reduced motion the dialog opens and closes without its fade and pop. Rows never animate.
- The highlight is only a background wash, which forced colors repaints, so in Windows high contrast the highlighted row has no mark of its own. Rows do not grow on a touch screen.
When to use something else
- Command palette for an app-wide ⌘K with a lot to find. It adds scopes, lists inside the palette, and a preview of the highlighted row, and it leaves the search to you, so the rows can come from a server.
- Combobox to choose a value for a field, like an assignee or a set of labels. It is this list in a Popover under its trigger, and it holds what was chosen, where Command runs an action and holds nothing.
- Dropdown menu for a few actions on one thing, where typing to filter would not help.
Install
shadcn add @gesso/commandIt brings Dialog for CommandDialog.