Skip to content

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

FixDevs ·

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.

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">🏠</span>
              <span>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 className="text-lg">⚙️</span>
              <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</span>
          <span>↵ Select</span>
          <span>Esc 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.

For related UI component issues, see Fix: Radix UI Not Working and Fix: shadcn/ui 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