Fix: cmdk Not Working — Command Palette Not Opening, Items Not Filtering, or Keyboard Navigation Broken
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.
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>
);
}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.
For related UI component issues, see Fix: Radix UI Not Working and Fix: shadcn/ui 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.