cmdk is a fast, unstyled command menu React component that provides a composable API for building command palettes, search interfaces, and accessible comboboxes. It automatically handles filtering, sorting, keyboard navigation, and accessibility, making it perfect for creating ⌘K-style command menus. This guide walks through setting up and creating command menus using cmdk with React, from installation to a working implementation.
Prerequisites
Before you begin, make sure you have:
- Node.js version 14.0 or higher installed
- npm, yarn, or pnpm package manager
- A React project (version 16.8 or higher) or create-react-app setup
- Basic knowledge of React hooks (useState, useEffect)
- Familiarity with JavaScript/TypeScript
- Understanding of keyboard navigation
Installation
Install cmdk using your preferred package manager:
npm install cmdk
Or with yarn:
yarn add cmdk
Or with pnpm:
pnpm add cmdk
After installation, your package.json should include:
{
"dependencies": {
"cmdk": "^1.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}
Project Setup
cmdk requires minimal setup. Import the components and you're ready to use them. You may want to add some basic styling for a better appearance.
First Example / Basic Usage
Let's create a simple command menu. Create a new file src/CommandMenuExample.jsx:
// src/CommandMenuExample.jsx
import React, { useState } from 'react';
import { Command } from 'cmdk';
function CommandMenuExample() {
const [open, setOpen] = useState(false);
return (
<div style={{ padding: '20px' }}>
<button
onClick={() => setOpen(!open)}
style={{
padding: '8px 16px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Open Command Menu
</button>
{open && (
<div style={{
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '500px',
backgroundColor: 'white',
borderRadius: '8px',
boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
zIndex: 1000
}}>
<Command>
<Command.Input placeholder="Type a command or search..." />
<Command.List>
<Command.Empty>No results found.</Command.Empty>
<Command.Group heading="Suggestions">
<Command.Item>Calendar</Command.Item>
<Command.Item>Search Emoji</Command.Item>
<Command.Item>Calculator</Command.Item>
</Command.Group>
<Command.Group heading="Settings">
<Command.Item>Profile</Command.Item>
<Command.Item>Billing</Command.Item>
<Command.Item>Settings</Command.Item>
</Command.Group>
</Command.List>
</Command>
</div>
)}
{open && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
zIndex: 999
}}
onClick={() => setOpen(false)}
/>
)}
</div>
);
}
export default CommandMenuExample;
Update your App.jsx:
// src/App.jsx
import React from 'react';
import CommandMenuExample from './CommandMenuExample';
import './App.css';
function App() {
return (
<div className="App">
<CommandMenuExample />
</div>
);
}
export default App;
This creates a basic command menu with searchable items grouped by category.
Understanding the Basics
cmdk provides command menu components:
- Command: Main container component
- Command.Input: Search input field
- Command.List: Container for command items
- Command.Item: Individual command item
- Command.Group: Group of related items
- Command.Empty: Message when no results found
- Command.Separator: Visual separator between groups
Key concepts:
- Automatic filtering: Items are filtered as you type
- Keyboard navigation: Arrow keys, Enter, Escape work automatically
- Composable API: Build complex menus with nested components
- Accessibility: Built-in ARIA attributes and keyboard support
- Unstyled: You provide all styling
Here's an example with more features:
// src/AdvancedCommandMenuExample.jsx
import React, { useState } from 'react';
import { Command } from 'cmdk';
function AdvancedCommandMenuExample() {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState('');
const items = [
{ id: '1', label: 'New File', group: 'File' },
{ id: '2', label: 'Open File', group: 'File' },
{ id: '3', label: 'Save', group: 'File' },
{ id: '4', label: 'Cut', group: 'Edit' },
{ id: '5', label: 'Copy', group: 'Edit' },
{ id: '6', label: 'Paste', group: 'Edit' },
{ id: '7', label: 'Undo', group: 'Edit' },
{ id: '8', label: 'Redo', group: 'Edit' }
];
const filteredItems = items.filter(item =>
item.label.toLowerCase().includes(search.toLowerCase())
);
const groupedItems = filteredItems.reduce((acc, item) => {
if (!acc[item.group]) {
acc[item.group] = [];
}
acc[item.group].push(item);
return acc;
}, {});
return (
<div style={{ padding: '20px' }}>
<button
onClick={() => setOpen(!open)}
style={{
padding: '8px 16px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Open Command Menu (⌘K)
</button>
{open && (
<div style={{
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '500px',
backgroundColor: 'white',
borderRadius: '8px',
boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
zIndex: 1000
}}>
<Command>
<Command.Input
placeholder="Type a command or search..."
value={search}
onValueChange={setSearch}
/>
<Command.List>
<Command.Empty>No results found.</Command.Empty>
{Object.entries(groupedItems).map(([group, groupItems]) => (
<Command.Group key={group} heading={group}>
{groupItems.map(item => (
<Command.Item
key={item.id}
onSelect={() => {
console.log(`Selected: ${item.label}`);
setOpen(false);
}}
>
{item.label}
</Command.Item>
))}
</Command.Group>
))}
</Command.List>
</Command>
</div>
)}
{open && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
zIndex: 999
}}
onClick={() => setOpen(false)}
/>
)}
</div>
);
}
export default AdvancedCommandMenuExample;
Practical Example / Building Something Real
Let's build a comprehensive command palette with keyboard shortcuts and actions:
// src/CommandPalette.jsx
import React, { useState, useEffect } from 'react';
import { Command } from 'cmdk';
function CommandPalette() {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState('');
useEffect(() => {
const down = (e) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((open) => !open);
}
if (e.key === 'Escape') {
setOpen(false);
}
};
document.addEventListener('keydown', down);
return () => document.removeEventListener('keydown', down);
}, []);
const commands = [
{
id: 'new-file',
label: 'New File',
shortcut: '⌘N',
group: 'File',
action: () => console.log('New file created')
},
{
id: 'open-file',
label: 'Open File',
shortcut: '⌘O',
group: 'File',
action: () => console.log('Open file dialog')
},
{
id: 'save',
label: 'Save',
shortcut: '⌘S',
group: 'File',
action: () => console.log('File saved')
},
{
id: 'cut',
label: 'Cut',
shortcut: '⌘X',
group: 'Edit',
action: () => console.log('Cut to clipboard')
},
{
id: 'copy',
label: 'Copy',
shortcut: '⌘C',
group: 'Edit',
action: () => console.log('Copied to clipboard')
},
{
id: 'paste',
label: 'Paste',
shortcut: '⌘V',
group: 'Edit',
action: () => console.log('Pasted from clipboard')
},
{
id: 'settings',
label: 'Settings',
shortcut: '⌘,',
group: 'Preferences',
action: () => console.log('Open settings')
},
{
id: 'theme',
label: 'Toggle Theme',
shortcut: '⌘T',
group: 'Preferences',
action: () => console.log('Theme toggled')
}
];
const filteredCommands = commands.filter(cmd =>
cmd.label.toLowerCase().includes(search.toLowerCase()) ||
cmd.group.toLowerCase().includes(search.toLowerCase())
);
const groupedCommands = filteredCommands.reduce((acc, cmd) => {
if (!acc[cmd.group]) {
acc[cmd.group] = [];
}
acc[cmd.group].push(cmd);
return acc;
}, {});
const handleSelect = (command) => {
command.action();
setOpen(false);
setSearch('');
};
return (
<>
{open && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
zIndex: 999,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
onClick={() => setOpen(false)}
>
<div
style={{
width: '600px',
maxWidth: '90vw',
backgroundColor: 'white',
borderRadius: '8px',
boxShadow: '0 10px 25px rgba(0,0,0,0.2)',
overflow: 'hidden'
}}
onClick={(e) => e.stopPropagation()}
>
<Command>
<Command.Input
placeholder="Type a command or search..."
value={search}
onValueChange={setSearch}
style={{
width: '100%',
padding: '12px',
border: 'none',
outline: 'none',
fontSize: '16px',
borderBottom: '1px solid #eee'
}}
/>
<Command.List style={{ maxHeight: '400px', overflow: 'auto' }}>
<Command.Empty style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
No results found.
</Command.Empty>
{Object.entries(groupedCommands).map(([group, groupCommands]) => (
<Command.Group
key={group}
heading={group}
style={{
padding: '8px 0',
fontSize: '12px',
fontWeight: 'bold',
color: '#666',
textTransform: 'uppercase',
paddingLeft: '12px'
}}
>
{groupCommands.map(cmd => (
<Command.Item
key={cmd.id}
onSelect={() => handleSelect(cmd)}
style={{
padding: '12px',
cursor: 'pointer',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}
>
<span>{cmd.label}</span>
<span style={{ fontSize: '12px', color: '#999' }}>
{cmd.shortcut}
</span>
</Command.Item>
))}
</Command.Group>
))}
</Command.List>
</Command>
</div>
</div>
)}
</>
);
}
export default CommandPalette;
Now create a full application with the command palette:
// src/AppWithCommandPalette.jsx
import React from 'react';
import CommandPalette from './CommandPalette';
function AppWithCommandPalette() {
return (
<div style={{ padding: '40px', minHeight: '100vh' }}>
<h1>My Application</h1>
<p>Press ⌘K (or Ctrl+K) to open the command palette</p>
<CommandPalette />
</div>
);
}
export default AppWithCommandPalette;
Update your App.jsx:
// src/App.jsx
import React from 'react';
import AppWithCommandPalette from './AppWithCommandPalette';
import './App.css';
function App() {
return (
<div className="App">
<AppWithCommandPalette />
</div>
);
}
export default App;
This example demonstrates:
- Keyboard shortcut (⌘K) to open menu
- Searchable command items
- Grouped commands
- Keyboard shortcuts display
- Action handlers
- Modal overlay
- Escape key to close
- Click outside to close
Common Issues / Troubleshooting
Menu not opening: Make sure you're handling the keyboard event correctly. Check that
openstate is being set properly.Items not filtering: Ensure
Command.InputhasonValueChangehandler that updates search state. Items are automatically filtered based on their text content.Styling issues: cmdk is unstyled by default. Add your own CSS or inline styles. Use
Command.ListwithmaxHeightandoverflow: autofor scrollable lists.Keyboard navigation not working: Make sure
Command.Itemcomponents are properly nested insideCommand.List. Check that no other elements are intercepting keyboard events.Accessibility issues: cmdk includes ARIA attributes by default. Ensure your custom styling doesn't break accessibility. Test with screen readers.
Performance with many items: For large lists, consider virtualizing items or implementing pagination. cmdk handles filtering efficiently, but rendering many DOM elements can be slow.
Next Steps
Now that you have an understanding of cmdk:
- Explore advanced filtering options
- Learn about custom item rendering
- Implement nested command groups
- Add icons to command items
- Create context-aware commands
- Integrate with routing libraries
- Check the official documentation: https://cmdk.paco.me/
Summary
You've successfully set up cmdk in your React application and created command menus with keyboard shortcuts, search functionality, and grouped commands. cmdk provides a fast, accessible, and composable solution for building command palettes in React applications.
SEO Keywords
cmdk
cmdk React
cmdk command menu
React command palette
cmdk installation
React ⌘K menu
cmdk tutorial
React command menu component
cmdk example
React command palette library
cmdk setup
React keyboard navigation
cmdk getting started
React searchable menu
cmdk advanced usage

Top comments (0)