Skip to content
Utilities

cmdk — Component Library Guide

Fast, composable, unstyled command menu React component. Powers the command palette in Linear, Vercel, Raycast, and shadcn/ui's Command component.

TypeScript MIT
1.4M weekly downloads
10.2k GitHub stars
~5kB bundle size
v1.0.4 latest version
command-palettesearchkeyboardaccessiblecomposable

TL;DR: cmdk is a headless, keyboard-navigable command palette for React. Add your commands as items, open it with Cmd+K, and users get instant fuzzy-filtered search. Used in Linear, Vercel, and shadcn/ui's Command component. Official docs: cmdk.paco.me.

What is cmdk?

cmdk is the component behind shadcn/ui's <Command> primitive - a fully accessible, keyboard-driven command palette. It renders an input with fuzzy-filtered results that the user navigates with arrow keys. Used by Linear, Vercel, Raycast, and countless other products.

When to use it

Add a command palette when your app has:

  • Many navigation options that are hard to discover via menus
  • Power users who prefer keyboard-driven workflows
  • A global search requirement (Cmd+K / Ctrl+K)

It also works well as a combobox/autocomplete replacement for complex selection UIs.

Key Features

  • Fuzzy filtering - built-in substring matching; override with custom filter function
  • Composable - Command.Group, Command.Separator, Command.Empty for structure
  • Accessible - ARIA combobox pattern, keyboard navigation, screen reader support
  • Controlled/uncontrolled - value + onValueChange for controlled mode
  • Async data - works with any data source; just update the items

Installation

npm install cmdk

Basic Command Palette

import {
  Command,
  CommandInput,
  CommandList,
  CommandEmpty,
  CommandGroup,
  CommandItem,
  CommandSeparator,
} from 'cmdk'
import { useState } from 'react'

function CommandPalette() {
  const [open, setOpen] = useState(false)

  // Open on Cmd+K / Ctrl+K
  useEffect(() => {
    function onKeyDown(e: KeyboardEvent) {
      if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
        e.preventDefault()
        setOpen(prev => !prev)
      }
    }
    document.addEventListener('keydown', onKeyDown)
    return () => document.removeEventListener('keydown', onKeyDown)
  }, [])

  if (!open) return null

  return (
    <div className="fixed inset-0 bg-black/40 flex items-start justify-center pt-[20vh]">
      <Command className="w-full max-w-lg bg-white rounded-xl shadow-2xl overflow-hidden border">
        <CommandInput placeholder="Type a command or search..." className="px-4 py-3 text-sm outline-none w-full border-b" />
        <CommandList className="max-h-72 overflow-y-auto p-2">
          <CommandEmpty className="py-6 text-center text-sm text-slate-400">
            No results found.
          </CommandEmpty>
          <CommandGroup heading="Navigation" className="text-xs text-slate-400 px-2 pb-1">
            <CommandItem onSelect={() => { navigate('/dashboard'); setOpen(false) }}
              className="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer aria-selected:bg-slate-100"
            >
              Dashboard
            </CommandItem>
            <CommandItem onSelect={() => { navigate('/settings'); setOpen(false) }}
              className="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer aria-selected:bg-slate-100"
            >
              Settings
            </CommandItem>
          </CommandGroup>
          <CommandSeparator />
          <CommandGroup heading="Actions">
            <CommandItem onSelect={() => setOpen(false)}
              className="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer aria-selected:bg-slate-100"
            >
              New document
            </CommandItem>
          </CommandGroup>
        </CommandList>
      </Command>
    </div>
  )
}

Custom Filter Function

// Use a better fuzzy matching library
import { matchSorter } from 'match-sorter'

<Command
  filter={(value, search) => {
    const results = matchSorter([value], search)
    return results.length > 0 ? 1 : 0
  }}
>

TypeScript Tips

Type your command items when building data-driven palettes:

interface CommandAction {
  id: string
  label: string
  keywords?: string[]
  onSelect: () => void
}

function buildCommands(actions: CommandAction[]) {
  return actions.map(action => (
    <CommandItem
      key={action.id}
      value={action.label}
      keywords={action.keywords}
      onSelect={action.onSelect}
    >
      {action.label}
    </CommandItem>
  ))
}

Common Gotchas

  • value prop on CommandItem - this is what the filter matches against, not what's displayed. If your label has emoji or extra markup, set value to the plain text string
  • Groups don't filter themselves - empty groups after filtering still render their heading. Use CommandGroup wrapped in a visibility check or use the forceMount approach
  • shadcn/ui Command - if you're on shadcn/ui, use its pre-styled Command component instead of raw cmdk; it handles the styling layer for you

Why command palettes win over deep menus

Think about how you actually find things in Linear or VS Code. You don't hunt through nested menus. You hit Cmd+K, type three letters, and hit enter. That's the whole appeal. A command palette flattens your entire app into one searchable surface, so the cost of adding a new action stays constant no matter how big the app grows.

For users, the payoff is muscle memory. Once someone learns Cmd+K, every action feels equally close. For developers, it's a discoverability tool. Features buried three menus deep suddenly get used because they show up in search. In my experience, adding a palette to an admin-heavy tool changes how people work within a week.

Structuring commands as data

Hard-coding CommandItem elements works for a demo. For a real app, drive the palette from a data array. Each command becomes an object with an id, a label, optional keywords, and a handler. This lets you generate items from your route table, your feature flags, or a permission check, so users only see commands they can actually run.

Keywords are the secret weapon here. The visible label might say "Sign out", but a user might type "logout" or "exit". Add those as keywords and the filter still finds the command. Spend ten minutes adding synonyms and your palette feels much smarter than it really is.

Putting it inside an accessible dialog

A palette is a modal, so it needs proper dialog behavior: focus trapping, an escape-to-close handler, and focus returning to the trigger element on close. cmdk handles the listbox keyboard model - arrow keys, enter to select, the ARIA combobox roles - but it doesn't draw a modal for you. Wrap it in a real dialog primitive so screen reader users get the right announcements and keyboard focus doesn't leak to the page behind it.

Don't forget the overlay click-to-close and a visible focus ring on the selected item. The aria-selected attribute is set for you on the active item, so style that with a clear background. Keyboard users need to see where they are without a mouse hover to guide them.

Static command lists filter instantly on the client. The moment your results come from an API - searching documents, users, or tickets - you switch to controlled mode. Track the input value in state, debounce it by 200 to 300 milliseconds so you don't fire a request per keystroke, fetch, then render the returned items inside CommandList.

Show a loading state while the request is in flight, and turn off cmdk's built-in filtering by setting shouldFilter={false} - the server already did the matching, so client filtering would just hide valid results. One last thing: cancel stale requests. A user typing fast can get an old slow response landing after a newer one, which shows wrong results. Aborting in-flight requests fixes that flicker.

  • React Select - for standard dropdown select inputs; cmdk is for keyboard-driven command/search interfaces
  • shadcn/ui - shadcn/ui's Command component is built directly on cmdk
  • React Hook Form - combine cmdk's Combobox pattern with React Hook Form for searchable form fields

Frequently Asked Questions

What is a command palette and when should I add one to my app?

A command palette is a keyboard-driven search interface, typically opened with Cmd+K or Ctrl+K, that lets users navigate and trigger actions without using the mouse. Add one when your app has many routes or actions that are hard to discover through traditional menus, or when you want to support power users.

How do I open and close the cmdk palette with a keyboard shortcut?

Add a keydown event listener to document that checks for metaKey or ctrlKey combined with the k key, then toggle your open state. Render the Command component conditionally based on that state, or use it inside a Dialog for proper modal behaviour.

What is the difference between cmdk and the shadcn/ui Command component?

The shadcn/ui Command component is cmdk with Tailwind CSS styling applied. If you are on shadcn/ui, use the pre-styled Command component. If you need full styling control, use cmdk directly and apply your own styles.

How do I add async search results to cmdk?

Control the input value with a state variable, debounce it, fetch results from your API, and update the items rendered inside CommandList. Use CommandLoading to show a loading indicator while the request is in flight.

Can I use cmdk for a select or combobox instead of a full command palette?

Yes. cmdk works well as a combobox or searchable select. Render it inline without a Dialog wrapper and control its open state based on focus or a trigger button. The keyboard navigation and filtering work identically.