Command

A command palette is one search field over everything an app can do.

A command palette is a search field over a list of rows, in a dialog. People open it with a shortcut, type a few letters, and press Enter. It covers the app and traps focus like a Dialog, and running a row closes it. Your app decides which rows exist and what each one does. To pick a value for a form, use a Combobox.

Playground

Run a command and it shows here.

Usage

import * as React from 'react';
import { Command, CommandContent, CommandEmpty, CommandInput, CommandItem, CommandList } from '@kushagradhawan/kookie-ui';
 
type Action = { label: string; run: () => void };
 
export function CommandPalette({ actions }: { actions: Action[] }) {
  const [open, setOpen] = React.useState(false);
 
  React.useEffect(() => {
    const onKey = (event: KeyboardEvent) => {
      if (event.key.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey)) {
        event.preventDefault();
        setOpen((value) => !value);
      }
    };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, []);
 
  return (
    <Command items={actions} open={open} onOpenChange={setOpen}>
      <CommandContent aria-label="Run a command">
        <CommandInput aria-label="Search commands" placeholder="Search commands" />
        <CommandList>
          {(action: Action) => (
            <CommandItem key={action.label} value={action.label} onSelect={action.run}>
              {action.label}
            </CommandItem>
          )}
        </CommandList>
        <CommandEmpty>No command matches.</CommandEmpty>
      </CommandContent>
    </Command>
  );
}

Anatomy

<Command items={items}>
  <CommandTrigger />
  <CommandContent>
    <CommandInput />
    <CommandList>
      {(group) => (
        <CommandGroup items={group.items}>
          <CommandGroupLabel />
          <CommandCollection>{(item) => <CommandItem />}</CommandCollection>
        </CommandGroup>
      )}
    </CommandList>
    <CommandEmpty />
  </CommandContent>
</Command>

CommandTrigger and the group parts are optional. Most palettes open from a key handler and render no trigger.

Best practices

Open it with a shortcut. A palette is for people who already know what they want. Bind ⌘K and Ctrl K, and add a toolbar button for people who haven't learned the shortcut.

Name each row as an action. "Invite a teammate" says what happens. "Team" doesn't.

Keep items in a stable order. Typing narrows the list and never reorders it, so a row people use every day stays in the same place.

Rank the results yourself when the list is long. Pass filter={null}, read the query from onQueryChange, and pass the ranked rows as items. The docs search on this site works this way.

Make a row a link when it goes to a page. Wrap your router's link in a row with asChild. Enter, a press and a middle-click then all behave like a link.

Size

size sets the palette's step, from 1 to 4. The rows are one step larger than a control at the same size, because the palette is the one thing on screen while it is open. When you leave size unset, the palette follows the nearest Theme size.

Examples

Groups

An item with its own items array is a group. A group with no matching rows is left out, and its label names it for a screen reader.

const groups = [
  { label: 'Project', items: projectActions },
  { label: 'App', items: appActions },
];
 
<Command items={groups} open={open} onOpenChange={setOpen}>
  <CommandContent aria-label="Run a command">
    <CommandInput aria-label="Search commands" placeholder="Search commands" />
    <CommandList>
      {(group: Group) => (
        <CommandGroup key={group.label} items={group.items}>
          <CommandGroupLabel>{group.label}</CommandGroupLabel>
          <CommandCollection>
            {(action: Action) => (
              <CommandItem key={action.label} value={action.label} onSelect={action.run}>
                {action.label}
              </CommandItem>
            )}
          </CommandCollection>
        </CommandGroup>
      )}
    </CommandList>
  </CommandContent>
</Command>

A destructive command with a shortcut

leading takes an icon, and trailing takes a shortcut. Set tone="destructive" on a row that deletes something.

<CommandItem
  value="Delete project"
  leading={<HugeiconsIcon icon={Delete02Icon} strokeWidth={1.75} />}
  trailing={<Kbd>⌘⌫</Kbd>}
  tone="destructive"
  onSelect={deleteProject}
>
  Delete project
</CommandItem>
<Command items={results} open={open} onOpenChange={setOpen}>
  <CommandContent aria-label="Search the documentation" filter={null} onQueryChange={setQuery}>
    <CommandInput aria-label="Search query" placeholder="Type to search" />
    <CommandList>
      {(page: Page) => (
        <CommandItem key={page.href} value={page.href} asChild>
          <Link href={page.href}>{page.title}</Link>
        </CommandItem>
      )}
    </CommandList>
  </CommandContent>
</Command>

Props

Command

PropTypeDefaultDescription
itemsreadonly unknown[]Required. Every item the palette can show, before filtering. An item with its own items array is a group
openbooleanWhether the palette is open, when your app controls it
defaultOpenbooleanfalseWhether the palette starts open, when it controls itself
onOpenChange(open, { reason }) => voidCalled when the palette opens or closes. reason is 'item-press' when a row ran, otherwise 'dismiss'
size'1' | '2' | '3' | '4'Theme sizeThe palette's step

CommandContent

PropTypeDefaultDescription
aria-labelstringRequired. The palette's accessible name
filter(item, query, itemToString) => boolean | nullContains the queryHow an item matches the query. null turns filtering off
itemToString(item) => stringThe text the filter reads. By default a string, or an object's label, title, value or name
onQueryChange(query: string) => voidCalled on every keystroke, and with '' when the palette closes
containerElementA container of your own for the portal

CommandInput

PropTypeDefaultDescription
aria-labelstringRequired. The field's accessible name
leadingReactNodeAn icon before the text

It also accepts the attributes of the HTML input element, such as placeholder.

CommandItem

PropTypeDefaultDescription
valueunknownThe item the row shows. A string is also the row's identity for the highlight
onSelect() => voidRuns when the row is pressed or Enter is pressed on it. The palette then closes
leadingReactNodeAn icon before the label
trailingReactNodeSomething after the label, such as a shortcut
tone'destructive'Set when running the row destroys something
asChildbooleanfalseRenders the child, such as your router's link, as the row
disabledbooleanfalseSkips the row

Other parts

PartProps
CommandTriggerWraps one child, which opens the palette
CommandListchildren: a function from an item to a CommandItem or a CommandGroup
CommandGroupitems: the group's own items
CommandGroupLabelchildren: the group's name
CommandCollectionchildren: a function from one of the group's items to a CommandItem
CommandEmptychildren: what the results pane says when nothing matches

Accessibility

  • Dialog: The palette is a modal dialog named by aria-label. Focus is trapped inside it, the page behind is inert, and Escape closes it. Focus returns to where it was.
  • Focus: The palette opens with the caret in the field. Focus stays in the field while the arrow keys move the highlight.
  • Keyboard: Up and Down move the highlight and wrap at the ends. Enter runs the highlighted row. A row is highlighted from the first frame and after every keystroke.
  • Groups: A group is named by its CommandGroupLabel.
  • Shortcuts: A shortcut in trailing is read as part of the row. The palette doesn't bind it. Bind it in your app.