Command Palette
A keyboard-driven command menu (⌘K style) for quick navigation and actions. Built using Box, Column, Row, Icon, Text, and TextField components.
or press ⌘K
Source
'use client';
import { Search } from 'lucide-react';
import { useEffect, useState } from 'react';
import { Box, Column, Icon, Row, Text, TextField } from '@umami/react-zen';
export function CommandPalette({
groups,
isOpen,
onClose,
placeholder = 'Search commands...',
}) {
const [search, setSearch] = useState('');
const [selectedIndex, setSelectedIndex] = useState(0);
const filteredGroups = groups
.map((group) => ({
...group,
items: group.items.filter(
(item) =>
item.label.toLowerCase().includes(search.toLowerCase()) ||
item.description?.toLowerCase().includes(search.toLowerCase())
),
}))
.filter((group) => group.items.length > 0);
const allItems = filteredGroups.flatMap((group) => group.items);
useEffect(() => {
if (!isOpen) {
setSearch('');
setSelectedIndex(0);
}
}, [isOpen]);
useEffect(() => {
const handleKeyDown = (e) => {
if (!isOpen) return;
if (e.key === 'Escape') {
onClose();
} else if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedIndex((i) => (i + 1) % allItems.length);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedIndex((i) => (i - 1 + allItems.length) % allItems.length);
} else if (e.key === 'Enter' && allItems[selectedIndex]) {
allItems[selectedIndex].onSelect();
onClose();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose, allItems, selectedIndex]);
if (!isOpen) return null;
let itemIndex = -1;
return (
<Column position="fixed" style={{ inset: 0, zIndex: 50 }} alignItems="center" paddingTop="20">
<Box position="absolute" style={{ inset: 0 }} backgroundColor="overlay" onClick={onClose} />
<Box position="relative" width="100%" maxWidth="32rem" backgroundColor="surface-raised" borderRadius="lg" border borderColor="muted" overflow="hidden">
<Row padding="3" gap="3" alignItems="center" borderColor="muted" border="bottom">
<Icon color="muted"><Search /></Icon>
<TextField value={search} onChange={setSearch} placeholder={placeholder} autoFocus style={{ border: 'none', background: 'transparent', outline: 'none', flex: 1 }} />
</Row>
<Column maxHeight="20rem" overflow="auto">
{filteredGroups.length === 0 ? (
<Box padding="4" textAlign="center"><Text color="muted">No results found</Text></Box>
) : (
filteredGroups.map((group) => (
<Column key={group.label}>
<Box paddingX="3" paddingY="2">
<Text color="muted" weight="semibold">{group.label}</Text>
</Box>
{group.items.map((item) => {
itemIndex++;
const isSelected = itemIndex === selectedIndex;
return (
<Row key={item.id} paddingX="3" paddingY="2" gap="3" alignItems="center" backgroundColor={isSelected ? 'interactive' : undefined} className="cursor-pointer" onClick={() => { item.onSelect(); onClose(); }}>
{item.icon && <Icon size="sm" color="muted">{item.icon}</Icon>}
<Column flexGrow="1" gap="0">
<Text>{item.label}</Text>
{item.description && <Text color="muted">{item.description}</Text>}
</Column>
{item.shortcut && (
<Row gap="1">
{item.shortcut.split('+').map((key, i) => (
<Box key={i} paddingX="2" paddingY="1" backgroundColor="surface-base" borderRadius="sm" border borderColor="muted">
<Text color="muted">{key}</Text>
</Box>
))}
</Row>
)}
</Row>
);
})}
</Column>
))
)}
</Column>
</Box>
</Column>
);
}
export function useCommandPalette() {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
const handleKeyDown = (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
setIsOpen((open) => !open);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
return {
isOpen,
open: () => setIsOpen(true),
close: () => setIsOpen(false),
toggle: () => setIsOpen((open) => !open),
};
}Features
- Keyboard navigation: Arrow keys to navigate, Enter to select, Escape to close
- Global shortcut: Press ⌘K (or Ctrl+K) to toggle
- Search filtering: Type to filter commands by label or description
- Grouped commands: Organize commands into logical groups
- Keyboard shortcuts: Display shortcuts for each command
Variations
With descriptions
Quick Actions
Create new projectStart a new project from scratch
Import from GitHubImport an existing repository
Browse templatesStart with a pre-built template