Fix: cmdk Not Working — Command Palette Not Opening, Items Not Filtering, or Keyboard Navigation Broken
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+KOr 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 itemsOr keyboard up/down navigation doesn’t highlight items:
Arrow keys don't move selection, Enter doesn't trigger onSelectWhy This Happens
cmdk is a command palette component for React. It provides the headless behavior (filtering, keyboard navigation, selection) but requires correct wiring:
Command.Dialogneeds explicitopenandonOpenChangestate — cmdk doesn’t manage its own open state or listen for keyboard shortcuts. You must handleCmd+Kyourself and pass the state to the Dialog.- Filtering uses the
valueprop — eachCommand.Itemneeds avaluestring. cmdk filters items by comparing the search input against item values. If items don’t havevalueprops, filtering uses the text content, which may not match expectations. - The component must be in a valid DOM context —
Command.Dialogrenders 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
onSelecthandlers — withoutonSelect, 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.Dialogwrapper. Filtering was string-only, nokeywordsprop, noCommand.Loading, and the Dialog had no portal customization. - cmdk 0.2.x (late 2022) — added
keywordsfor aliases,Command.Loadingfor 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
disablePointerSelectionso 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.Dialognow uses Radix Dialog under the hood, andvaul(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 commandcopies a pre-styled wrapper into your project. The shadcn wrapper assumes Tailwind v3+ withtailwindcss-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>
);
}Fix 4: Async Search
'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 invisible — Command.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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Conform Not Working — Form Validation Not Triggering, Server Errors Missing, or Zod Schema Rejected
How to fix Conform form validation issues — useForm setup with Zod, server action integration, nested and array fields, file uploads, progressive enhancement, and Remix and Next.js usage.
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.
Fix: Lingui Not Working — Messages Not Extracted, Translations Missing, or Macro Errors
How to fix Lingui.js i18n issues — setup with React, message extraction, macro compilation, ICU format, lazy loading catalogs, and Next.js integration.
Fix: Mantine Not Working — Styles Not Loading, Theme Not Applying, or Components Broken After Upgrade
How to fix Mantine UI issues — MantineProvider setup, PostCSS configuration, theme customization, dark mode, form validation with useForm, and Next.js App Router integration.