React

A command menu with shadcn

shadcn's Command is a filterable list built on cmdk: put it in a Dialog, open it with ⌘K, and give every item a unique value.

Key points

  • shadcn's Command wraps cmdk: filtering, selection and keyboard navigation come from the library.
  • `value` is the string cmdk searches: two entries with the same text need explicit values.
  • With `shouldFilter={false}` the server filters, and the list stays correct with async data.

Short answer: npx shadcn@latest add command installs a wrapper around cmdk; put Command inside CommandDialog, open it with ⌘K, and give every CommandItem a unique value. The library handles filtering and keyboard navigation.

The parts of the menu

This component is not a dropdown: it is a list of commands with a search field. Every part has one job, and cmdk applies the ARIA roles, not your CSS.

PartRole
CommandDialogThe modal shell: mounts Command inside a Dialog and adds a title and description for screen readers.
CommandInputThe search field with the combobox role; it takes focus when the menu opens.
CommandListThe listbox container, scrollable when the items exceed the height.
CommandItemOne selectable entry; its text or explicit value feeds the filter.
CommandEmptyThe message shown when no entry matches.
CommandGroup / CommandSeparatorGroup entries and split blocks of commands.

Opening with ⌘K, without surprises

The shortcut is registered on document inside an effect. preventDefault() matters: without it, Ctrl+K on Windows and ⌘K on macOS trigger the browser search. The same gesture has to work inside a text field too, so do not filter on event.target.

"use client";

import { useEffect, useState } from "react";
import {
  CommandDialog,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
  CommandShortcut,
} from "@/components/ui/command";

export function CommandMenu() {
  const [open, setOpen] = useState(false);

  useEffect(() => {
    function onKeyDown(event: KeyboardEvent) {
      if (event.key === "k" && (event.metaKey || event.ctrlKey)) {
        event.preventDefault();
        setOpen((value) => !value);
      }
    }
    document.addEventListener("keydown", onKeyDown);
    return () => document.removeEventListener("keydown", onKeyDown);
  }, []);

  return (
    
      
      
        Nessun risultato.
        
           setOpen(false)}>
            Dashboard
            G D
          
           setOpen(false)}>
            Impostazioni
          
        
        
        
           setOpen(false)}>
            Esporta CSV
          
        
      
    
  );
}

The filter: where it goes wrong

cmdk compares what you type against each entry's value. Without value it reads the node text: two entries called “Open” produce two identical results and selection turns ambiguous. The fix costs one line: a descriptive value, with the visible label left free.

When entries arrive from a remote search, the local filter fights the server results: you type “re” and cmdk hides the entries the server has just returned. In that case pass shouldFilter={false} and own the list, the loading state and the empty state yourself.


  
  
    {loading && Caricamento…}
    {!loading && results.length === 0 && (
      Nessun utente trovato.
    )}
    
      {results.map((user) => (
         pick(user)}>
          {user.name}
        
      ))}
    
  

Accessibility

shadcn's CommandDialog accepts title and description and renders them in a hidden header, because a Radix Dialog without a title breaks assisted navigation. If you build the dialog by hand, add DialogTitle: the case is covered in DialogContent requires DialogTitle. The list is already fine: arrows, Home, End and Enter work without extra code.

A named limitation

cmdk filters in memory, in the browser. With a few hundred entries it feels instant; with tens of thousands the cost lands on the main thread and a remote filter stops being optional. The catalogue, measured on 1 November 2026, holds 19 command blocks: every one of them assumes a short, local list.

Where to continue

In the catalogue you find a command menu block, the command component record from Origin UI and the shadcn library. For the setup read what shadcn/ui is and how to add a shadcn block; the catalogue comparison is in 21st, shadcnspace and the others.

Frequently asked questions

Does Command need an external library?

Yes, cmdk: the shadcn component is a wrapper with the design system's styling. Install it with npx shadcn@latest add command and the dependency arrives with the file.

Why do some entries never show up in search?

Because the filter matches value, not only the visible text. When value is an id or a different string, search that; or set value to the label and keep the id in a separate field.

How do I open the menu with ⌘K?

Add a keydown listener on document, check event.metaKey or event.ctrlKey with key === "k", and call preventDefault. Register the listener inside an effect and remove it on cleanup.

Can I filter results on the server?

Yes. Pass shouldFilter={false}, keep query in controlled state, and own the list, the loading state and the empty state. The local filter otherwise hides the results that just arrived.

Does the menu work with a keyboard?

Yes: cmdk handles arrows, Home, End, Enter and Escape. You only have to give the Dialog a title, because without DialogTitle assisted navigation loses context.