Search commands… ⌘K
import {
CalculatorIcon,
CalendarIcon,
CreditCardIcon,
SettingsIcon,
SmileIcon,
UserIcon,
} from "lucide-react" ;
import { useEffect, useState } from "react" ;
import { Button } from "@/components/ui/button" ;
import {
CommandItem,
CommandPalette,
CommandSection,
CommandSeparator,
CommandShortcut,
} from "@/components/ui/command-palette" ;
import { Kbd } from "@/components/ui/kbd" ;
export default function CommandPaletteDemo () {
const [ isOpen , setOpen ] = useState ( false );
useEffect (() => {
const onKeyDown = ( e : KeyboardEvent ) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e. preventDefault ();
setOpen (( open ) => ! open);
}
};
window. addEventListener ( "keydown" , onKeyDown);
return () => window. removeEventListener ( "keydown" , onKeyDown);
}, []);
return (
<>
< Button
variant = "outline"
className = "text-muted-foreground"
onPress = {() => setOpen ( true )}
>
Search commands… < Kbd >⌘K</ Kbd >
</ Button >
< CommandPalette isOpen = {isOpen} onOpenChange = {setOpen}>
< CommandSection title = "Suggestions" >
< CommandItem textValue = "Calendar" >
< CalendarIcon /> Calendar
</ CommandItem >
< CommandItem textValue = "Search emoji" >
< SmileIcon /> Search emoji
</ CommandItem >
< CommandItem textValue = "Calculator" isDisabled >
< CalculatorIcon /> Calculator
</ CommandItem >
</ CommandSection >
< CommandSeparator />
< CommandSection title = "Settings" >
< CommandItem textValue = "Profile" >
< UserIcon /> Profile < CommandShortcut >⌘P</ CommandShortcut >
</ CommandItem >
< CommandItem textValue = "Billing" >
< CreditCardIcon /> Billing < CommandShortcut >⌘B</ CommandShortcut >
</ CommandItem >
< CommandItem textValue = "Settings" >
< SettingsIcon /> Settings < CommandShortcut >⌘S</ CommandShortcut >
</ CommandItem >
</ CommandSection >
</ CommandPalette >
</>
);
}
CLI Manual
$ pnpm dlx shadcn@latest add @desyne/command-palette
The CLI installs dependencies and any other components this one uses.
import {
CommandItem,
CommandPalette,
CommandSection,
CommandSeparator,
CommandShortcut,
} from "@/components/ui/command-palette" ;
const [ isOpen , setOpen ] = useState ( false );
< CommandPalette
isOpen = {isOpen}
onOpenChange = {setOpen}
onAction = {( key ) => run (key)}
>
< CommandItem id = "new-project" >New project</ CommandItem >
< CommandItem id = "invite" >Invite teammate</ CommandItem >
</ CommandPalette >
Control the palette with isOpen and onOpenChange, and bind the ⌘ +K shortcut yourself with a keydown listener (see the demo's code).
The palette closes itself after a command runs: once onAction (or an item's own onAction) has been called, it calls onOpenChange(false). Links (href items) close it too. Pass closeOnAction={false} to keep it open, for example for multi-step commands, and close it yourself with setOpen(false) when you're done.
Command Palette — a global, searchable list of actions and destinations, usually on ⌘ +K . Also good for any "pick one from many" step, like assigning a teammate.
Menu — a short, contextual list of actions attached to a button.
Combo Box — a searchable form field that keeps a value.
Search Field — searching content on the page rather than running commands.
< CommandPalette > { /* overlay + modal + dialog + search input + menu */ }
< CommandSection title = "…" > { /* optional group with a heading */ }
< CommandItem > { /* a command */ }
< Icon />
Label
< CommandShortcut /> { /* keyboard hint */ }
</ CommandItem >
</ CommandSection >
< CommandSeparator />
</ CommandPalette >
Part Renders Notes CommandPaletteModalOverlay › Modal › Dialog › AutocompleteBackdrop, a panel near the top of the screen (pt-[15vh]), a search input and a role="menu" list that scrolls after 320px. Search input SearchField › <input type="search">Auto-focused, with aria-label="Search commands" and a search icon. CommandItem<div role="menuitem">A command. Same component as MenuItem : variant, href, isDisabled and render props all work. CommandSection<section role="group">Groups items under an optional title. Hidden when none of its items match. CommandSeparator<hr>Divider between sections. CommandShortcut<kbd>Right-aligned shortcut hint. The palette doesn't bind it. Empty state <div>emptyMessage, shown when nothing matches.
Pass items and a render function instead of static children. Each item needs an id; onAction receives it. Only the items that match the query are rendered.
import { useState } from "react" ;
import { Button } from "@/components/ui/button" ;
import { CommandItem, CommandPalette } from "@/components/ui/command-palette" ;
const pages = [
{ id: "dashboard" , name: "Dashboard" },
{ id: "projects" , name: "Projects" },
{ id: "team" , name: "Team members" },
{ id: "invoices" , name: "Invoices" },
{ id: "reports" , name: "Reports" },
{ id: "settings" , name: "Settings" },
];
export default function CommandPaletteDynamic () {
const [ isOpen , setOpen ] = useState ( false );
const [ last , setLast ] = useState < string >();
return (
< div className = "flex flex-col items-center gap-3" >
< Button variant = "outline" onPress = {() => setOpen ( true )}>
Jump to page
</ Button >
{last && (
< p className = "text-muted-foreground text-sm" >Navigated to: {last}</ p >
)}
< CommandPalette
isOpen = {isOpen}
onOpenChange = {setOpen}
items = {pages}
placeholder = "Search pages…"
onAction = {( key ) => {
setLast (pages. find (( p ) => p.id === key)?.name);
}}
>
{( page ) => < CommandItem >{page.name}</ CommandItem >}
</ CommandPalette >
</ div >
);
}
Put React Aria's Text with slot="label" and slot="description" inside a CommandItem for two-line results. The description is announced separately, and filtering still uses textValue.
Search workspace
import { FileTextIcon, FolderKanbanIcon, UserRoundIcon } from "lucide-react" ;
import { useState } from "react" ;
import { Text } from "react-aria-components" ;
import { Button } from "@/components/ui/button" ;
import {
CommandItem,
CommandPalette,
CommandSection,
} from "@/components/ui/command-palette" ;
export default function CommandPaletteRichItems () {
const [ isOpen , setOpen ] = useState ( false );
return (
<>
< Button variant = "outline" onPress = {() => setOpen ( true )}>
Search workspace
</ Button >
< CommandPalette
isOpen = {isOpen}
onOpenChange = {setOpen}
placeholder = "Search projects, docs and people…"
>
< CommandSection title = "Projects" >
< CommandItem textValue = "Website relaunch" >
< FolderKanbanIcon />
< span className = "flex min-w-0 flex-col" >
< Text slot = "label" >Website relaunch</ Text >
< Text
slot = "description"
className = "text-muted-foreground text-xs"
>
Marketing · 24 open issues
</ Text >
</ span >
</ CommandItem >
< CommandItem textValue = "Billing v2" >
< FolderKanbanIcon />
< span className = "flex min-w-0 flex-col" >
< Text slot = "label" >Billing v2</ Text >
< Text
slot = "description"
className = "text-muted-foreground text-xs"
>
Platform · 9 open issues
</ Text >
</ span >
</ CommandItem >
</ CommandSection >
< CommandSection title = "Docs" >
< CommandItem textValue = "Incident response runbook" >
< FileTextIcon />
< span className = "flex min-w-0 flex-col" >
< Text slot = "label" >Incident response runbook</ Text >
< Text
slot = "description"
className = "text-muted-foreground text-xs"
>
Updated 2 days ago by Priya
</ Text >
</ span >
</ CommandItem >
</ CommandSection >
< CommandSection title = "People" >
< CommandItem textValue = "Marcus Lee" >
< UserRoundIcon />
< span className = "flex min-w-0 flex-col" >
< Text slot = "label" >Marcus Lee</ Text >
< Text
slot = "description"
className = "text-muted-foreground text-xs"
>
Engineering manager
</ Text >
</ span >
</ CommandItem >
</ CommandSection >
</ CommandPalette >
</>
);
}
Filtering matches the query anywhere in an item's textValue, ignoring case and accents. Add synonyms to textValue so "dark" finds Appearance or "2fa" finds Security. The item's accessible name still comes from its visible content.
Open settings search Try “dark”, “2fa” or “timezone”
import {
BellIcon,
KeyRoundIcon,
LanguagesIcon,
PaletteIcon,
} from "lucide-react" ;
import { useState } from "react" ;
import { Button } from "@/components/ui/button" ;
import { CommandItem, CommandPalette } from "@/components/ui/command-palette" ;
const settings = [
{
id: "appearance" ,
name: "Appearance" ,
keywords: "theme dark light mode colors" ,
icon: PaletteIcon,
},
{
id: "notifications" ,
name: "Notifications" ,
keywords: "email alerts push digest" ,
icon: BellIcon,
},
{
id: "security" ,
name: "Security" ,
keywords: "password 2fa two-factor passkey sessions" ,
icon: KeyRoundIcon,
},
{
id: "language" ,
name: "Language & region" ,
keywords: "locale timezone date format" ,
icon: LanguagesIcon,
},
];
export default function CommandPaletteKeywords () {
const [ isOpen , setOpen ] = useState ( false );
return (
< div className = "flex flex-col items-center gap-2" >
< Button variant = "outline" onPress = {() => setOpen ( true )}>
Open settings search
</ Button >
< p className = "text-muted-foreground text-xs" >
Try “dark”, “2fa” or “timezone”
</ p >
< CommandPalette
isOpen = {isOpen}
onOpenChange = {setOpen}
items = {settings}
placeholder = "Search settings…"
>
{( s ) => (
< CommandItem textValue = { `${ s . name } ${ s . keywords }` }>
< s.icon /> {s.name}
</ CommandItem >
)}
</ CommandPalette >
</ div >
);
}
Give an item an href to navigate. Same-page anchors, external URLs with target="_blank", and routes through React Aria's RouterProvider all work.
Open resources
import {
BookOpenIcon,
ExternalLinkIcon,
GitBranchIcon,
LifeBuoyIcon,
NewspaperIcon,
} from "lucide-react" ;
import { useState } from "react" ;
import { Button } from "@/components/ui/button" ;
import {
CommandItem,
CommandPalette,
CommandSection,
} from "@/components/ui/command-palette" ;
export default function CommandPaletteLinks () {
const [ isOpen , setOpen ] = useState ( false );
return (
<>
< Button variant = "outline" onPress = {() => setOpen ( true )}>
Open resources
</ Button >
< CommandPalette
isOpen = {isOpen}
onOpenChange = {setOpen}
placeholder = "Jump to a resource…"
>
< CommandSection title = "On this page" >
< CommandItem textValue = "Accessibility" href = "#accessibility" >
< LifeBuoyIcon /> Accessibility
</ CommandItem >
< CommandItem textValue = "API reference" href = "#api-reference" >
< BookOpenIcon /> API reference
</ CommandItem >
</ CommandSection >
< CommandSection title = "External" >
< CommandItem
textValue = "React Aria Autocomplete"
href = "https://react-spectrum.adobe.com/react-aria/Autocomplete.html"
target = "_blank"
>
< NewspaperIcon /> React Aria Autocomplete
< ExternalLinkIcon className = "ml-auto" />
</ CommandItem >
< CommandItem
textValue = "React Spectrum on GitHub"
href = "https://github.com/adobe/react-spectrum"
target = "_blank"
>
< GitBranchIcon /> React Spectrum on GitHub
< ExternalLinkIcon className = "ml-auto" />
</ CommandItem >
</ CommandSection >
</ CommandPalette >
</>
);
}
disabledKeys keeps commands visible but not selectable, and keyboard navigation skips them. Say why, for example with a hint on the item.
Run a command
import {
DatabaseBackupIcon,
RocketIcon,
RotateCcwIcon,
ScrollTextIcon,
} from "lucide-react" ;
import { useState } from "react" ;
import { Button } from "@/components/ui/button" ;
import { CommandItem, CommandPalette } from "@/components/ui/command-palette" ;
const actions = [
{ id: "deploy" , name: "Deploy to production" , icon: RocketIcon },
{ id: "rollback" , name: "Roll back last deploy" , icon: RotateCcwIcon },
{ id: "backup" , name: "Create database backup" , icon: DatabaseBackupIcon },
{ id: "logs" , name: "View build logs" , icon: ScrollTextIcon },
];
export default function CommandPaletteDisabled () {
const [ isOpen , setOpen ] = useState ( false );
return (
<>
< Button variant = "outline" onPress = {() => setOpen ( true )}>
Run a command
</ Button >
< CommandPalette
isOpen = {isOpen}
onOpenChange = {setOpen}
items = {actions}
disabledKeys = {[ "deploy" , "rollback" ]}
>
{( a ) => (
< CommandItem textValue = {a.name}>
< a.icon /> {a.name}
{(a.id === "deploy" || a.id === "rollback" ) && (
< span className = "ml-auto text-muted-foreground text-xs" >
Requires admin
</ span >
)}
</ CommandItem >
)}
</ CommandPalette >
</>
);
}
emptyMessage accepts any node. Use it to suggest what to try next. placeholder sets the input's hint, and aria-label names the dialog (it defaults to "Command palette").
Find customer
import { SearchXIcon } from "lucide-react" ;
import { useState } from "react" ;
import { Button } from "@/components/ui/button" ;
import { CommandItem, CommandPalette } from "@/components/ui/command-palette" ;
const customers = [
{ id: "c1" , name: "Brightline Logistics" },
{ id: "c2" , name: "Cedar & Stone" },
{ id: "c3" , name: "Harbor Health" },
{ id: "c4" , name: "Northwind Traders" },
];
export default function CommandPaletteEmptyState () {
const [ isOpen , setOpen ] = useState ( false );
return (
<>
< Button variant = "outline" onPress = {() => setOpen ( true )}>
Find customer
</ Button >
< CommandPalette
isOpen = {isOpen}
onOpenChange = {setOpen}
items = {customers}
aria-label = "Find customer"
placeholder = "Search customers by name…"
emptyMessage = {
< div className = "flex flex-col items-center gap-1" >
< SearchXIcon className = "size-5" />
< p className = "font-medium text-foreground" >No customers found</ p >
< p className = "text-xs" >Check the spelling, or search by email.</ p >
</ div >
}
>
{( c ) => < CommandItem >{c.name}</ CommandItem >}
</ CommandPalette >
</>
);
}
The classic ⌘ +K palette: a search-style trigger button with a Kbd hint, a global shortcut, recent items, actions with shortcuts and navigation. className="max-w-xl" widens the panel.
import {
ArrowRightIcon,
CreditCardIcon,
FileTextIcon,
FolderIcon,
LogOutIcon,
MoonIcon,
PlusIcon,
SearchIcon,
SettingsIcon,
UserPlusIcon,
} from "lucide-react" ;
import { useEffect, useState } from "react" ;
import type { Key } from "react-aria-components" ;
import { Button } from "@/components/ui/button" ;
import {
CommandItem,
CommandPalette,
CommandSection,
CommandSeparator,
CommandShortcut,
} from "@/components/ui/command-palette" ;
import { Kbd, KbdGroup } from "@/components/ui/kbd" ;
import { toast } from "@/components/ui/toast" ;
const labels : Record < string , string > = {
"recent-signup" : "Opened Signup funnel" ,
"recent-board" : "Opened Board metrics" ,
"new-report" : "New report created" ,
invite: "Invite dialog opened" ,
theme: "Switched to dark mode" ,
settings: "Went to Settings" ,
billing: "Went to Billing" ,
logout: "Signed out" ,
};
export default function CommandPaletteRecipeAppSearch () {
const [ isOpen , setOpen ] = useState ( false );
useEffect (() => {
const onKeyDown = ( e : KeyboardEvent ) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e. preventDefault ();
setOpen (( open ) => ! open);
}
};
window. addEventListener ( "keydown" , onKeyDown);
return () => window. removeEventListener ( "keydown" , onKeyDown);
}, []);
const run = ( key : Key ) => {
toast (labels[ String (key)]);
};
return (
<>
< Button
variant = "outline"
className = "w-full max-w-72 justify-start text-muted-foreground"
onPress = {() => setOpen ( true )}
>
< SearchIcon /> Search or jump to…
< KbdGroup className = "ml-auto" >
< Kbd size = "sm" >⌘</ Kbd >
< Kbd size = "sm" >K</ Kbd >
</ KbdGroup >
</ Button >
< CommandPalette
isOpen = {isOpen}
onOpenChange = {setOpen}
onAction = {run}
className = "max-w-xl"
>
< CommandSection title = "Recent" >
< CommandItem id = "recent-signup" textValue = "Signup funnel" >
< FileTextIcon /> Signup funnel
< span className = "ml-auto text-muted-foreground text-xs" >
Report
</ span >
</ CommandItem >
< CommandItem id = "recent-board" textValue = "Board metrics" >
< FolderIcon /> Board metrics
< span className = "ml-auto text-muted-foreground text-xs" >
Project
</ span >
</ CommandItem >
</ CommandSection >
< CommandSeparator />
< CommandSection title = "Actions" >
< CommandItem id = "new-report" textValue = "New report" >
< PlusIcon /> New report < CommandShortcut >⌘N</ CommandShortcut >
</ CommandItem >
< CommandItem id = "invite" textValue = "Invite teammate" >
< UserPlusIcon /> Invite teammate{ " " }
< CommandShortcut >⌘I</ CommandShortcut >
</ CommandItem >
< CommandItem id = "theme" textValue = "Toggle dark mode theme" >
< MoonIcon /> Toggle dark mode < CommandShortcut >⌘D</ CommandShortcut >
</ CommandItem >
</ CommandSection >
< CommandSeparator />
< CommandSection title = "Navigate" >
< CommandItem id = "settings" textValue = "Go to settings" >
< SettingsIcon /> Settings < ArrowRightIcon className = "ml-auto" />
</ CommandItem >
< CommandItem id = "billing" textValue = "Go to billing" >
< CreditCardIcon /> Billing < ArrowRightIcon className = "ml-auto" />
</ CommandItem >
< CommandItem
id = "logout"
textValue = "Sign out log out"
variant = "destructive"
>
< LogOutIcon /> Sign out
</ CommandItem >
</ CommandSection >
</ CommandPalette >
</>
);
}
A palette used as a picker: people with Avatar s, searchable by name or email through textValue, that sets the assignee on an issue card.
ENG-512
Retry failed webhook deliveries
High
import { UserRoundPlusIcon } from "lucide-react" ;
import { useState } from "react" ;
import { Avatar } from "@/components/ui/avatar" ;
import { Badge } from "@/components/ui/badge" ;
import { Button } from "@/components/ui/button" ;
import { CommandItem, CommandPalette } from "@/components/ui/command-palette" ;
const people = [
{ id: "ada" , name: "Ada Mensah" , email: "ada@lumen.app" , initials: "AM" },
{
id: "bruno" ,
name: "Bruno Costa" ,
email: "bruno@lumen.app" ,
initials: "BC" ,
},
{ id: "chen" , name: "Chen Wei" , email: "chen@lumen.app" , initials: "CW" },
{ id: "dara" , name: "Dara O'Neill" , email: "dara@lumen.app" , initials: "DO" },
{ id: "eli" , name: "Eli Rosen" , email: "eli@lumen.app" , initials: "ER" },
];
type Person = ( typeof people)[ number ];
export default function CommandPaletteRecipeAssign () {
const [ isOpen , setOpen ] = useState ( false );
const [ assignee , setAssignee ] = useState < Person | null >( null );
return (
< div className = "w-full max-w-sm rounded-xl border bg-card p-4" >
< div className = "flex items-start justify-between gap-3" >
< div >
< p className = "text-muted-foreground text-xs" >ENG-512</ p >
< p className = "mt-0.5 font-medium text-sm" >
Retry failed webhook deliveries
</ p >
</ div >
< Badge size = "sm" color = "warning" >
High
</ Badge >
</ div >
< div className = "mt-4 flex items-center justify-between" >
{assignee ? (
< span className = "flex items-center gap-2 text-sm" >
< Avatar
size = "sm"
alt = {assignee.name}
fallback = {assignee.initials}
colorful
/>
{assignee.name}
</ span >
) : (
< span className = "text-muted-foreground text-sm" >Unassigned</ span >
)}
< Button variant = "ghost" size = "sm" onPress = {() => setOpen ( true )}>
< UserRoundPlusIcon /> {assignee ? "Reassign" : "Assign" }
</ Button >
</ div >
< CommandPalette
isOpen = {isOpen}
onOpenChange = {setOpen}
items = {people}
aria-label = "Assign issue"
placeholder = "Assign to…"
emptyMessage = "No teammates match."
className = "max-w-md"
onAction = {( key ) => {
setAssignee (people. find (( p ) => p.id === key) ?? null );
}}
>
{( p ) => (
< CommandItem textValue = { `${ p . name } ${ p . email }` }>
< Avatar size = "xs" alt = {p.name} fallback = {p.initials} colorful />
{p.name}
< span className = "ml-auto text-muted-foreground text-xs" >
{p.email}
</ span >
</ CommandItem >
)}
</ CommandPalette >
</ div >
);
}
A narrow palette (max-w-sm) that switches the theme, with a check on the current option.
import {
CheckIcon,
MonitorIcon,
MoonIcon,
PaletteIcon,
SunIcon,
} from "lucide-react" ;
import { useState } from "react" ;
import { Button } from "@/components/ui/button" ;
import { CommandItem, CommandPalette } from "@/components/ui/command-palette" ;
const themes = [
{ id: "light" , name: "Light" , icon: SunIcon },
{ id: "dark" , name: "Dark" , icon: MoonIcon },
{ id: "system" , name: "System" , icon: MonitorIcon },
];
export default function CommandPaletteRecipeTheme () {
const [ isOpen , setOpen ] = useState ( false );
const [ theme , setTheme ] = useState ( "system" );
const current = themes. find (( t ) => t.id === theme);
return (
<>
< Button variant = "outline" onPress = {() => setOpen ( true )}>
< PaletteIcon /> Theme: {current?.name}
</ Button >
< CommandPalette
isOpen = {isOpen}
onOpenChange = {setOpen}
items = {themes}
aria-label = "Change theme"
placeholder = "Change theme…"
className = "max-w-sm"
onAction = {( key ) => {
setTheme ( String (key));
}}
>
{( t ) => (
< CommandItem textValue = {t.name}>
< t.icon /> {t.name}
{t.id === theme && (
< CheckIcon
aria-label = "Current theme"
className = "ml-auto text-foreground"
/>
)}
</ CommandItem >
)}
</ CommandPalette >
</>
);
}
The palette is a modal dialog: focus is trapped, the page is hidden from screen readers, and focus returns to the previously focused element on close.
Focus stays in the search input while ↑ / ↓ move a virtual focus through the results, so users can keep typing. The highlighted command is announced as it changes.
Name the dialog with aria-label when it has a specific purpose ("Assign issue"); the default is "Command palette".
Commands are menuitems with their text from textValue; section titles label their groups.
Shortcuts shown with CommandShortcut are hints only. Register the keys with your own handler.
Key Action Any character Filters the commands ↓ / ↑ Moves the highlight through the results Home / End Highlights the first / last result Page Up / Page Down Moves the highlight by a page Enter Runs the highlighted command Esc Clears the query, or closes the palette when the query is empty
On each CommandItem, the same attributes as MenuItem : data-focused for the highlighted result, data-disabled, data-hovered, data-pressed and data-variant. The panel has data-entering / data-exiting during its fade and zoom.
data-slotElement command-palette-overlayBackdrop command-palettePanel menu-item, menu-section, menu-separator, menu-shortcutItems and their parts
className styles the panel. It's w-full max-w-lg by default: use max-w-xl for a wider palette or max-w-sm for a focused picker.
The result list is capped at max-h-80 and scrolls; icons in items are sized and muted like in Menu .
The backdrop reuses overlayStyles from Dialog .
Also accepts the other props of React Aria's ModalOverlay . Filtering is provided by Autocomplete .
Re-exports of MenuItem , MenuSection , MenuSeparator and MenuShortcut. They accept the same props; CommandItem also takes textValue, which is what the query is matched against.
Menu — the same items in a dropdown.
Dialog — the modal the palette is built on.
Combo Box — a searchable field that keeps a value.
Kbd — show the shortcut that opens the palette.