Command

A command menu with search, groups and keyboard navigation. It's the "just type it" escape hatch for your app.

Preview

Usage

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
  CommandShortcut,
} from '@vitnode/core/components/ui/command'
<Command>
  <CommandInput placeholder="Type a command or search..." />
  <CommandList>
    <CommandEmpty>No results found.</CommandEmpty>
    <CommandGroup heading="Suggestions">
      <CommandItem>Calendar</CommandItem>
      <CommandItem>Search Emoji</CommandItem>
    </CommandGroup>
  </CommandList>
</Command>

Type and the list filters itself, best matches first. Empty groups hide. Built on cmdk, so every cmdk prop works.

Groups, icons and shortcuts

CommandGroup takes a heading, and CommandSeparator splits groups. Icons go first and are sized for you. CommandShortcut is only a label: bind the keys yourself.

<CommandGroup heading="Settings">
  <CommandItem>
    <UserIcon />
    <span>Profile</span>
    <CommandShortcut>⌘P</CommandShortcut>
  </CommandItem>
</CommandGroup>
<CommandSeparator />

CommandEmpty shows only when nothing matches. Make it kind.

Selecting items

onSelect fires on click and Enter. disabled keeps an item visible but skipped. Items match by their text: add value when the text isn't unique, and keywords for other names people might type.

<CommandItem onSelect={() => router.navigate({ to: '/settings' })}>
  Settings
</CommandItem>
<CommandItem keywords={['theme', 'night']} value="toggle-dark-mode">
  Toggle dark mode
</CommandItem>

Dialog

CommandDialog wraps a Command in a Dialog with a screen-reader-only title and description. Control it with open and onOpenChange, and bind a shortcut yourself.

const [open, setOpen] = React.useState(false)

React.useEffect(() => {
  const handleKeyDown = (event: KeyboardEvent) => {
    if (event.key.toLowerCase() !== 'j') return
    if (!event.metaKey && !event.ctrlKey) return
    if (event.altKey || event.shiftKey || event.defaultPrevented) return

    event.preventDefault()
    setOpen(prev => !prev)
  }

  window.addEventListener('keydown', handleKeyDown)

  return () => window.removeEventListener('keydown', handleKeyDown)
}, [])

<CommandDialog onOpenChange={setOpen} open={open}>
  <Command>
    <CommandInput placeholder="What do you need?" />
    <CommandList>...</CommandList>
  </Command>
</CommandDialog>

Why ⌘J and not ⌘K?

The docs search and the AdminCP already own ⌘K / Ctrl+K, so the demo uses ⌘J / Ctrl+J. Pick a free key and remove the listener on unmount. Inside the open palette Ctrl+J moves down a row, so Escape is the way out.

Show the shortcut next to the trigger with Kbd, like the demo.

Your own filtering

Searching an API? Set shouldFilter={false} and render what your query returns. The AdminCP search works this way.

<Command label="Search members" shouldFilter={false}>
  <CommandInput onValueChange={setSearch} value={search} />
  <CommandList>
    {members.map((member) => (
      <CommandItem key={member.id} value={member.id}>
        {member.name}
      </CommandItem>
    ))}
  </CommandList>
</Command>

Accessibility

  • The input is a combobox and the list a listbox, so screen readers announce the highlighted option.
  • ↑ / ↓ move, Home / End jump to the ends, Enter selects. loop wraps at the edges.
  • Ctrl+N / Ctrl+J and Ctrl+P /Ctrl+K also move. Turn them off with vimBindings={false}.
  • Give a standalone Command a label so the input has a name. CommandDialog traps focus and closes on Escape.

Props

Command

Prop

Type

CommandDialog

Prop

Type

CommandItem

Prop

Type

API Reference

cmdk