Building an accessible command palette in React

A command palette is deceptively small in scope — a text input and a filtered list — and deceptively large in what has to be correct for it to actually be accessible. Here’s the full list of what a Cmd/Ctrl+K palette needs to get right, and a working example that does.

What "accessible" means here, specifically

None of this is exotic — it’s the WAI-ARIA combobox pattern applied to a modal instead of a form field. But it’s a lot of small, easy-to-skip details, and skipping any one of them is invisible if you only test with a mouse.

A working example

<CommandMenu
  open={open}
  onOpenChange={setOpen}
  groups={[
    {
      heading: 'Actions',
      items: [
        { id: 'new', label: 'New file', shortcut: ['⌘', 'N'], onSelect: createFile },
        { id: 'search', label: 'Search docs', keywords: ['find'], onSelect: openSearch },
      ],
    },
  ]}
/>

That’s the whole surface: a list of grouped items, each doing exactly one of two things — running an onSelect callback, or (for multi-step palettes) opening a nested page of further commands. Every item should do exactly one of those, never both and never neither; a "New file" item that both runs an action and drills into a sub-page has no consistent Enter behavior for a screen-reader user to predict.

Two mistakes worth calling out

The fastest way to find out if any of this actually works: close your mouse and drive the whole flow — open, type, arrow down three results, Enter — from the keyboard alone. Every gap shows up immediately.