Skip to content

Fix: cmdk Not Working — Command Palette Not Opening, Items Not Filtering, or Keyboard Navigation Broken

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix cmdk command palette issues — Dialog setup, custom filtering, groups and separators, keyboard shortcuts, async search, nested pages, and integration with shadcn/ui and Tailwind.

The Problem

The command palette doesn’t open when pressing Cmd+K:

import { Command } from 'cmdk';

function CommandPalette() {
  return (
    <Command.Dialog open={true}>
      <Command.Input placeholder="Search..." />
      <Command.List>
        <Command.Item>Settings</Command.Item>
        <Command.Item>Profile</Command.Item>
      </Command.List>
    </Command.Dialog>
  );
}
// Nothing happens when pressing Cmd+K

Or items don’t filter when typing:

<Command.Input placeholder="Search..." />
<Command.List>
  <Command.Item value="settings">Settings</Command.Item>
  <Command.Item value="profile">Profile</Command.Item>
</Command.List>
// Typing "set" still shows both items

Or keyboard up/down navigation doesn’t highlight items:

Arrow keys don't move selection, Enter doesn't trigger onSelect

Why This Happens

cmdk is a command palette component for React. It provides the headless behavior (filtering, keyboard navigation, selection) but requires correct wiring:

  • Command.Dialog needs explicit open and onOpenChange state — cmdk doesn’t manage its own open state or listen for keyboard shortcuts. You must handle Cmd+K yourself and pass the state to the Dialog.
  • Filtering uses the value prop — each Command.Item needs a value string. cmdk filters items by comparing the search input against item values. If items don’t have value props, filtering uses the text content, which may not match expectations.
  • The component must be in a valid DOM contextCommand.Dialog renders a portal. If CSS or z-index issues hide the portal, the palette appears to not open. Similarly, if a parent catches keyboard events and stops propagation, navigation breaks.
  • Items need onSelect handlers — without onSelect, clicking or pressing Enter on an item does nothing. The visual selection (highlighting) works, but no action fires.

A second class of issues comes from the way cmdk renders. The library does not own the markup it sits inside. It exposes primitives (Command, Command.Input, Command.List, Command.Item) and lets you compose them, which means animation libraries, Radix Dialog wrappers, and overlay components can all interfere. If a parent applies display: none to hide the dialog instead of unmounting it, focus management breaks. If a portal target is moved around the DOM by another library, the keyboard handler attached to document may stop receiving events when the palette is open.

A third class is filtering semantics. cmdk does a substring match against the combined value and keywords of each item, lowercased and trimmed. If you render items with whitespace or React fragments inside, the visible text and the computed value diverge, so typing the visible label does not match. Asynchronous lists make this worse: items mounted after the query string changes are filtered against the current search, so a result that arrives 200ms late may never appear.

Version History (cmdk 0.x → 1.0 and the shadcn era)

cmdk has gone through a quiet but meaningful evolution since Paco Coursey released it in 2022, and which version you target changes both the API surface and the issues you hit.

  • cmdk 0.1.x (2022) — initial release, focused on a single Command.Dialog wrapper. Filtering was string-only, no keywords prop, no Command.Loading, and the Dialog had no portal customization.
  • cmdk 0.2.x (late 2022) — added keywords for aliases, Command.Loading for async states, shouldFilter={false} to disable built-in filtering, and improved typing. Most StackOverflow answers and blog posts you find today reference this API.
  • cmdk 0.2.10+ (early 2023) — fixed several Safari focus bugs and added disablePointerSelection so hover would not steal selection from keyboard nav. If you are stuck on an older 0.2 minor, upgrading inside the 0.2 range often resolves “selection jumps when moving the mouse”.
  • cmdk 1.0 (2023) — the first stable release. Internals were refactored to use Radix-style primitives (state machine + composition), Command.Dialog now uses Radix Dialog under the hood, and vaul (Paco’s drawer library) shares the same primitives. Breaking changes are minimal but the bundled Radix Dialog means you should not also wrap cmdk inside another Radix Dialog — focus traps fight each other.
  • cmdk 1.x with shadcn/ui (2023→) — shadcn/ui adopted cmdk as the canonical Command primitive. Running npx shadcn@latest add command copies a pre-styled wrapper into your project. The shadcn wrapper assumes Tailwind v3+ with tailwindcss-animate, so older Tailwind setups need to add the plugin or strip the animation classes.
  • Integration ecosystem (2024→) — Linear, Vercel dashboard, Raycast-like web palettes, and most modern Next.js admin panels use cmdk + shadcn. The pattern is stable enough that the API has not changed materially since 1.0.

Practical implication: if you are debugging a “not working” issue, check npm ls cmdk first. A project pinned to 0.1.x will be missing keywords and Command.Loading; a project on 1.x inside another Radix Dialog will have focus issues. Most working examples online assume 0.2 or 1.x.

Fix 1: Basic Command Palette with Keyboard Shortcut

npm install cmdk
'use client';

import { Command } from 'cmdk';
import { useEffect, useState } from 'react';

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

  // Handle Cmd+K / Ctrl+K
  useEffect(() => {
    function handleKeyDown(e: KeyboardEvent) {
      if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
        e.preventDefault();
        setOpen(prev => !prev);
      }
    }

    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, []);

  return (
    <Command.Dialog
      open={open}
      onOpenChange={setOpen}
      label="Command Menu"
    >
      <Command.Input placeholder="Type a command or search..." />

      <Command.List>
        <Command.Empty>No results found.</Command.Empty>

        <Command.Group heading="Navigation">
          <Command.Item
            value="home"
            onSelect={() => {
              window.location.href = '/';
              setOpen(false);
            }}
          >
            Home
          </Command.Item>
          <Command.Item
            value="dashboard"
            onSelect={() => {
              window.location.href = '/dashboard';
              setOpen(false);
            }}
          >
            Dashboard
          </Command.Item>
          <Command.Item
            value="settings"
            onSelect={() => {
              window.location.href = '/settings';
              setOpen(false);
            }}
          >
            Settings
          </Command.Item>
        </Command.Group>

        <Command.Separator />

        <Command.Group heading="Actions">
          <Command.Item
            value="new-project"
            onSelect={() => {
              createProject();
              setOpen(false);
            }}
          >
            Create New Project
          </Command.Item>
          <Command.Item
            value="invite-member"
            onSelect={() => {
              openInviteModal();
              setOpen(false);
            }}
          >
            Invite Team Member
          </Command.Item>
        </Command.Group>

        <Command.Group heading="Theme">
          <Command.Item value="light-mode" onSelect={() => setTheme('light')}>
            Light Mode
          </Command.Item>
          <Command.Item value="dark-mode" onSelect={() => setTheme('dark')}>
            Dark Mode
          </Command.Item>
        </Command.Group>
      </Command.List>
    </Command.Dialog>
  );
}

Fix 2: Styling with Tailwind CSS

cmdk is unstyled by default. Apply styles via CSS selectors or className:

import { Command } from 'cmdk';

function StyledCommandPalette({ open, setOpen }) {
  return (
    <Command.Dialog
      open={open}
      onOpenChange={setOpen}
      className="fixed inset-0 z-50"
    >
      {/* Overlay */}
      <div
        className="fixed inset-0 bg-black/50"
        onClick={() => setOpen(false)}
      />

      {/* Content */}
      <div className="fixed top-[20%] left-1/2 -translate-x-1/2 w-full max-w-lg bg-white dark:bg-gray-900 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-700 overflow-hidden">
        <Command.Input
          className="w-full px-4 py-3 text-lg border-b border-gray-200 dark:border-gray-700 bg-transparent outline-none placeholder:text-gray-400"
          placeholder="Type a command or search..."
        />

        <Command.List className="max-h-[300px] overflow-y-auto p-2">
          <Command.Empty className="py-6 text-center text-gray-500">
            No results found.
          </Command.Empty>

          <Command.Group
            heading="Navigation"
            className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-gray-500"
          >
            <Command.Item
              className="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer text-sm data-[selected=true]:bg-blue-50 dark:data-[selected=true]:bg-blue-900/20 data-[selected=true]:text-blue-600"
              value="home"
              onSelect={() => { /* ... */ }}
            >
              <span className="text-lg">Home</span>
              <kbd className="ml-auto text-xs text-gray-400 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded">
                G H
              </kbd>
            </Command.Item>

            <Command.Item
              className="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer text-sm data-[selected=true]:bg-blue-50 dark:data-[selected=true]:bg-blue-900/20 data-[selected=true]:text-blue-600"
              value="settings"
              onSelect={() => { /* ... */ }}
            >
              <span>Settings</span>
            </Command.Item>
          </Command.Group>
        </Command.List>

        {/* Footer with keyboard hints */}
        <div className="flex items-center justify-between px-4 py-2 border-t border-gray-200 dark:border-gray-700 text-xs text-gray-400">
          <span>Navigate with arrow keys</span>
          <span>Enter to select</span>
          <span>Esc to close</span>
        </div>
      </div>
    </Command.Dialog>
  );
}

Fix 3: Custom Filtering and Search Keywords

function AdvancedCommandPalette() {
  return (
    <Command
      // Custom filter function
      filter={(value, search) => {
        // Default: fuzzy match. Custom: exact substring match
        if (value.toLowerCase().includes(search.toLowerCase())) return 1;
        return 0;
      }}
    >
      <Command.Input placeholder="Search..." />
      <Command.List>
        {/* Keywords — additional search terms not shown in the UI */}
        <Command.Item
          value="settings"
          keywords={['preferences', 'config', 'options']}
          onSelect={() => navigate('/settings')}
        >
          Settings
        </Command.Item>

        {/* Searching "preferences" matches this item */}
        <Command.Item
          value="new-project"
          keywords={['create', 'add', 'start']}
          onSelect={() => openNewProjectModal()}
        >
          New Project
        </Command.Item>

        {/* Disable filtering for async search */}
        {/* Set shouldFilter={false} on Command root */}
      </Command.List>
    </Command>
  );
}
'use client';

import { Command } from 'cmdk';
import { useEffect, useState } from 'react';

function AsyncSearchPalette() {
  const [open, setOpen] = useState(false);
  const [search, setSearch] = useState('');
  const [results, setResults] = useState<SearchResult[]>([]);
  const [loading, setLoading] = useState(false);

  // Debounced search
  useEffect(() => {
    if (search.length < 2) {
      setResults([]);
      return;
    }

    setLoading(true);
    const timer = setTimeout(async () => {
      const res = await fetch(`/api/search?q=${encodeURIComponent(search)}`);
      const data = await res.json();
      setResults(data.results);
      setLoading(false);
    }, 300);

    return () => clearTimeout(timer);
  }, [search]);

  return (
    <Command.Dialog open={open} onOpenChange={setOpen} shouldFilter={false}>
      <Command.Input
        placeholder="Search everything..."
        value={search}
        onValueChange={setSearch}
      />

      <Command.List>
        {loading && <Command.Loading>Searching...</Command.Loading>}

        <Command.Empty>
          {search.length < 2 ? 'Type to search...' : 'No results found.'}
        </Command.Empty>

        {results.map(result => (
          <Command.Item
            key={result.id}
            value={result.id}
            onSelect={() => {
              navigate(result.url);
              setOpen(false);
            }}
          >
            <div>
              <p className="font-medium">{result.title}</p>
              <p className="text-sm text-gray-500">{result.description}</p>
            </div>
          </Command.Item>
        ))}
      </Command.List>
    </Command.Dialog>
  );
}

Fix 5: Nested Pages (Sub-Menus)

'use client';

import { Command } from 'cmdk';
import { useState } from 'react';

function NestedCommandPalette() {
  const [open, setOpen] = useState(false);
  const [pages, setPages] = useState<string[]>([]);
  const activePage = pages[pages.length - 1];

  return (
    <Command.Dialog open={open} onOpenChange={setOpen}>
      <Command.Input
        placeholder={
          activePage === 'projects' ? 'Search projects...' :
          activePage === 'team' ? 'Search team members...' :
          'What do you need?'
        }
      />

      <Command.List>
        {/* Root page */}
        {!activePage && (
          <>
            <Command.Item onSelect={() => setPages([...pages, 'projects'])}>
              Browse Projects
            </Command.Item>
            <Command.Item onSelect={() => setPages([...pages, 'team'])}>
              Team Members
            </Command.Item>
            <Command.Item onSelect={() => { navigate('/settings'); setOpen(false); }}>
              Settings
            </Command.Item>
          </>
        )}

        {/* Projects sub-page */}
        {activePage === 'projects' && (
          <>
            <Command.Item onSelect={() => { navigate('/projects/alpha'); setOpen(false); }}>
              Project Alpha
            </Command.Item>
            <Command.Item onSelect={() => { navigate('/projects/beta'); setOpen(false); }}>
              Project Beta
            </Command.Item>
          </>
        )}

        {/* Team sub-page */}
        {activePage === 'team' && (
          <>
            <Command.Item onSelect={() => { navigate('/team/alice'); setOpen(false); }}>
              Alice Johnson
            </Command.Item>
            <Command.Item onSelect={() => { navigate('/team/bob'); setOpen(false); }}>
              Bob Smith
            </Command.Item>
          </>
        )}
      </Command.List>

      {/* Back button */}
      {activePage && (
        <div className="border-t p-2">
          <button onClick={() => setPages(pages.slice(0, -1))}>
            Back
          </button>
        </div>
      )}
    </Command.Dialog>
  );
}

Fix 6: shadcn/ui Command Component

shadcn/ui wraps cmdk with pre-built styling:

npx shadcn@latest add command dialog
'use client';

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

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

  useEffect(() => {
    const down = (e: KeyboardEvent) => {
      if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
        e.preventDefault();
        setOpen(prev => !prev);
      }
    };
    document.addEventListener('keydown', down);
    return () => document.removeEventListener('keydown', down);
  }, []);

  return (
    <CommandDialog open={open} onOpenChange={setOpen}>
      <CommandInput placeholder="Type a command or search..." />
      <CommandList>
        <CommandEmpty>No results found.</CommandEmpty>
        <CommandGroup heading="Suggestions">
          <CommandItem onSelect={() => setOpen(false)}>Calendar</CommandItem>
          <CommandItem onSelect={() => setOpen(false)}>Search</CommandItem>
        </CommandGroup>
        <CommandSeparator />
        <CommandGroup heading="Settings">
          <CommandItem onSelect={() => setOpen(false)}>Profile</CommandItem>
          <CommandItem onSelect={() => setOpen(false)}>Billing</CommandItem>
        </CommandGroup>
      </CommandList>
    </CommandDialog>
  );
}

Still Not Working?

Dialog doesn’t open on Cmd+K — cmdk doesn’t handle keyboard shortcuts. You must add your own keydown listener that sets open to true. Make sure the listener is on document, not a specific element, and that e.preventDefault() is called to prevent the browser’s default Cmd+K behavior (which opens the search bar in some browsers).

Items show but filtering doesn’t work — each Command.Item needs a value prop. Without it, cmdk uses the text content, but whitespace and nested elements can cause unexpected matching. Set explicit value strings. For async search, set shouldFilter={false} on the Command root and handle filtering yourself.

Keyboard navigation skips items — disabled items (disabled prop) are skipped. Also check that items aren’t conditionally rendered in a way that removes them from the DOM during navigation. Use Command.Item’s forceMount if items should remain in the list while hidden.

Dialog renders but is invisibleCommand.Dialog renders through a portal. Check z-index, and make sure no parent has overflow: hidden that clips the portal. Add a background overlay and inspect the DOM to verify the dialog is present but hidden by CSS.

Focus traps fight each other inside another Radix Dialog — cmdk 1.x uses Radix Dialog internally. If you wrap Command.Dialog inside another Radix Dialog.Root, both focus traps activate and arrow keys may not reach the list. Render the palette at the root of your app, not inside an existing dialog.

value and visible label disagree — cmdk computes value from the text content if you do not pass value explicitly. Items that render React elements (icons, spans, kbd hints) end up with a value that contains all of that text. Always set an explicit value prop on every Command.Item you intend to filter.

Async results never appear because filtering is on — when results come from fetch, the default filter runs against whatever you pass in. Set shouldFilter={false} on the Command root and trust your server to filter. Otherwise items that arrive after the user keeps typing get filtered against the current query and silently disappear.

For related UI component issues, see Fix: Radix UI Not Working and Fix: shadcn/ui Not Working. For related accessibility and animation libraries that often sit next to cmdk, see Fix: React Aria Not Working and Fix: Framer Motion Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles