UI/UI
Command Menu
A command palette component with keyboard navigation, search, and shortcuts for quick actions and navigation.
import * as React from "react";
import {
CommandMenu,
CommandMenuTrigger,
CommandMenuContent,
CommandMenuInput,
CommandMenuList,
CommandMenuGroup,
CommandMenuItem,
CommandMenuSeparator,
useCommandMenuShortcut,
} from "@/components/ui/command-menu";
import { Button } from "@/components/ui/button";
import { Kbd } from "@/components/ui/kbd";
import {
Command,
Calendar,
User,
Settings,
Plus,
Upload,
Download
} from "lucide-react";
// Utility function to detect OS and return appropriate modifier key
const getModifierKey = () => {
if (typeof navigator === "undefined") return { key: "Ctrl", symbol: "Ctrl" };
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0 ||
navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
return isMac
? { key: "cmd", symbol: "⌘" }
: { key: "ctrl", symbol: "Ctrl" };
};
export const BasicCommandMenu = () => {
const [open, setOpen] = React.useState(false);
useCommandMenuShortcut(() => setOpen(true));
return (
<CommandMenu open={open} onOpenChange={setOpen}>
<CommandMenuTrigger asChild>
<Button variant="outline" className="gap-2">
<Command size={16} />
Open Command Menu
<div className="ml-auto flex items-center gap-1">
<Kbd size="xs">{getModifierKey().symbol}</Kbd>
<Kbd size="xs">K</Kbd>
</div>
</Button>
</CommandMenuTrigger>
<CommandMenuContent>
<CommandMenuInput placeholder="Type a command or search..." />
<CommandMenuList>
<CommandMenuGroup heading="Suggestions">
<CommandMenuItem icon={<Calendar />} index={0}>
Calendar
</CommandMenuItem>
<CommandMenuItem icon={<User />} index={1}>
Search Users
</CommandMenuItem>
<CommandMenuItem icon={<Settings />} index={2}>
Settings
</CommandMenuItem>
</CommandMenuGroup>
<CommandMenuSeparator />
<CommandMenuGroup heading="Actions">
<CommandMenuItem icon={<Plus />} index={3} shortcut="cmd+n">
Create New
</CommandMenuItem>
<CommandMenuItem icon={<Upload />} index={4} shortcut="cmd+u">
Upload File
</CommandMenuItem>
<CommandMenuItem icon={<Download />} index={5} shortcut="cmd+d">
Download
</CommandMenuItem>
</CommandMenuGroup>
</CommandMenuList>
</CommandMenuContent>
</CommandMenu>
);
};
Installation
Install following dependencies:
npm install @radix-ui/react-dialog @radix-ui/react-visually-hidden motion lucide-react
pnpm add @radix-ui/react-dialog @radix-ui/react-visually-hidden motion lucide-react
yarn add @radix-ui/react-dialog @radix-ui/react-visually-hidden motion lucide-react
bun add @radix-ui/react-dialog @radix-ui/react-visually-hidden motion lucide-react
Copy and paste the following code into your project.
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
import { motion } from "motion/react";
import { Search, X } from "lucide-react";
import { cn } from "@/lib/utils";
import { Kbd } from "@/components/ui/kbd";
// Utility function to detect OS and return appropriate modifier key
const getModifierKey = () => {
if (typeof navigator === "undefined") return { key: "Ctrl", symbol: "Ctrl" };
const isMac =
navigator.platform.toUpperCase().indexOf("MAC") >= 0 ||
navigator.userAgent.toUpperCase().indexOf("MAC") >= 0;
return isMac ? { key: "cmd", symbol: "⌘" } : { key: "ctrl", symbol: "Ctrl" };
};
// Context for sharing state between components
interface CommandMenuContextType {
value: string;
setValue: (value: string) => void;
selectedIndex: number;
setSelectedIndex: (index: number) => void;
}
const CommandMenuContext = React.createContext<
CommandMenuContextType | undefined
>(undefined);
const CommandMenuProvider: React.FC<{
children: React.ReactNode;
value: string;
setValue: (value: string) => void;
selectedIndex: number;
setSelectedIndex: (index: number) => void;
}> = ({ children, value, setValue, selectedIndex, setSelectedIndex }) => (
<CommandMenuContext.Provider
value={{ value, setValue, selectedIndex, setSelectedIndex }}
>
{children}
</CommandMenuContext.Provider>
);
const useCommandMenu = () => {
const context = React.useContext(CommandMenuContext);
if (!context) {
throw new Error("useCommandMenu must be used within CommandMenuProvider");
}
return context;
};
// Core CommandMenu component using Dialog
const CommandMenu = DialogPrimitive.Root;
const CommandMenuTrigger = DialogPrimitive.Trigger;
const CommandMenuPortal = DialogPrimitive.Portal;
const CommandMenuClose = DialogPrimitive.Close;
// Title components for accessibility
const CommandMenuTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight text-[hsl(var(--hu-foreground))]",
className,
)}
{...props}
/>
));
CommandMenuTitle.displayName = "CommandMenuTitle";
const CommandMenuDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-[hsl(var(--hu-muted-foreground))]", className)}
{...props}
/>
));
CommandMenuDescription.displayName = "CommandMenuDescription";
// Overlay with backdrop blur
const CommandMenuOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
));
CommandMenuOverlay.displayName = "CommandMenuOverlay";
// Main content container with keyboard navigation
const CommandMenuContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
showShortcut?: boolean;
}
>(({ className, children, showShortcut = true, ...props }, ref) => {
const [value, setValue] = React.useState("");
const [selectedIndex, setSelectedIndex] = React.useState(0);
// Keyboard navigation
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "ArrowDown") {
e.preventDefault();
// Logic will be handled by CommandMenuList
} else if (e.key === "ArrowUp") {
e.preventDefault();
// Logic will be handled by CommandMenuList
} else if (e.key === "Enter") {
e.preventDefault();
// Logic will be handled by CommandMenuItem
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, []);
return (
<CommandMenuPortal>
<CommandMenuOverlay />
<DialogPrimitive.Content asChild ref={ref} {...props}>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: -20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: -20 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className={cn(
"fixed left-[50%] top-[30%] z-50 w-[95%] max-w-2xl translate-x-[-50%] translate-y-[-50%]",
"bg-[hsl(var(--hu-background))] border border-[hsl(var(--hu-border))] rounded-2xl shadow-2xl",
"overflow-hidden",
className,
)}
>
<CommandMenuProvider
value={value}
setValue={setValue}
selectedIndex={selectedIndex}
setSelectedIndex={setSelectedIndex}
>
{/* Hidden title for accessibility - required by Radix UI */}
<VisuallyHidden.Root>
<CommandMenuTitle>Command Menu</CommandMenuTitle>
</VisuallyHidden.Root>
{children}
<CommandMenuClose className="absolute right-3 top-3 rounded-lg p-1.5 text-[hsl(var(--hu-muted-foreground))] hover:text-[hsl(var(--hu-foreground))] hover:bg-[hsl(var(--hu-accent))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--hu-ring))] transition-colors">
<X size={14} />
<span className="sr-only">Close</span>
</CommandMenuClose>
{showShortcut && (
<div className="absolute right-12 top-3 flex items-center justify-center gap-1 h-6.5">
<Kbd size="xs">{getModifierKey().symbol}</Kbd>
<Kbd size="xs">K</Kbd>
</div>
)}
</CommandMenuProvider>
</motion.div>
</DialogPrimitive.Content>
</CommandMenuPortal>
);
});
CommandMenuContent.displayName = "CommandMenuContent";
// Input component for search
const CommandMenuInput = React.forwardRef<
HTMLInputElement,
React.InputHTMLAttributes<HTMLInputElement> & {
placeholder?: string;
}
>(
(
{ className, placeholder = "Type a command or search...", ...props },
ref,
) => {
const { value, setValue } = useCommandMenu();
return (
<div className="flex items-center border-b border-[hsl(var(--hu-border))] px-3 py-0">
<Search className="mr-3 h-4 w-4 shrink-0 text-[hsl(var(--hu-muted-foreground))]" />
<input
ref={ref}
value={value}
onChange={(e) => setValue(e.target.value)}
className={cn(
"flex h-12 w-full rounded-none border-0 bg-transparent py-3 text-sm outline-none placeholder:text-[hsl(var(--hu-muted-foreground))] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
placeholder={placeholder}
{...props}
/>
</div>
);
},
);
CommandMenuInput.displayName = "CommandMenuInput";
// List container for command items
const CommandMenuList = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & {
maxHeight?: string;
}
>(({ className, children, maxHeight = "300px", ...props }, ref) => {
const { selectedIndex, setSelectedIndex } = useCommandMenu();
// Handle keyboard navigation
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const items = document.querySelectorAll("[data-command-item]");
const maxIndex = items.length - 1;
if (e.key === "ArrowDown") {
e.preventDefault();
setSelectedIndex(Math.min(selectedIndex + 1, maxIndex));
} else if (e.key === "ArrowUp") {
e.preventDefault();
setSelectedIndex(Math.max(selectedIndex - 1, 0));
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [selectedIndex, setSelectedIndex]);
return (
<div
ref={ref}
className={cn("max-h-[300px] overflow-y-auto p-1", className)}
style={{ maxHeight }}
{...props}
>
{children}
</div>
);
});
CommandMenuList.displayName = "CommandMenuList";
// Command group with optional heading
const CommandMenuGroup = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & {
heading?: string;
}
>(({ className, children, heading, ...props }, ref) => (
<div ref={ref} className={cn("", className)} {...props}>
{heading && (
<div className="px-2 py-1.5 text-xs font-medium text-[hsl(var(--hu-muted-foreground))] uppercase tracking-wider">
{heading}
</div>
)}
{children}
</div>
));
CommandMenuGroup.displayName = "CommandMenuGroup";
// Individual command item
const CommandMenuItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & {
onSelect?: () => void;
disabled?: boolean;
shortcut?: string;
icon?: React.ReactNode;
index?: number;
}
>(
(
{
className,
children,
onSelect,
disabled = false,
shortcut,
icon,
index = 0,
...props
},
ref,
) => {
const { selectedIndex, setSelectedIndex } = useCommandMenu();
const isSelected = selectedIndex === index;
// Handle click and enter key
const handleSelect = React.useCallback(() => {
if (!disabled && onSelect) {
onSelect();
}
}, [disabled, onSelect]);
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter" && isSelected) {
e.preventDefault();
handleSelect();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isSelected, handleSelect]);
return (
<div
ref={ref}
data-command-item
className={cn(
"relative flex cursor-default select-none items-center rounded-[var(--radius)] px-2 py-2 text-sm outline-none transition-colors",
"hover:bg-[hsl(var(--hu-accent))] hover:text-[hsl(var(--hu-accent-foreground))]",
isSelected &&
"bg-[hsl(var(--hu-accent))] text-[hsl(var(--hu-accent-foreground))]",
disabled && "pointer-events-none opacity-50",
className,
)}
onClick={handleSelect}
onMouseEnter={() => setSelectedIndex(index)}
{...props}
>
{icon && (
<div className="mr-2 h-4 w-4 flex items-center justify-center">
{icon}
</div>
)}
<div className="flex-1">{children}</div>
{shortcut && (
<div className="ml-auto flex items-center gap-1">
{shortcut.split("+").map((key, i) => (
<React.Fragment key={key}>
{i > 0 && (
<span className="text-[hsl(var(--hu-muted-foreground))] text-xs">
+
</span>
)}
<Kbd size="xs">
{key === "cmd" || key === "⌘"
? getModifierKey().symbol
: key === "shift"
? "⇧"
: key === "alt"
? "⌥"
: key === "ctrl"
? getModifierKey().key === "cmd"
? "⌃"
: "Ctrl"
: key}
</Kbd>
</React.Fragment>
))}
</div>
)}
</div>
);
},
);
CommandMenuItem.displayName = "CommandMenuItem";
// Separator between groups
const CommandMenuSeparator = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("-mx-1 my-1 h-px bg-[hsl(var(--hu-border))]", className)}
{...props}
/>
));
CommandMenuSeparator.displayName = "CommandMenuSeparator";
// Empty state
const CommandMenuEmpty = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, children = "No results found.", ...props }, ref) => (
<div
ref={ref}
className={cn(
"py-6 text-center text-sm text-[hsl(var(--hu-muted-foreground))]",
className,
)}
{...props}
>
{children}
</div>
));
CommandMenuEmpty.displayName = "CommandMenuEmpty";
// Hook for global keyboard shortcut
export const useCommandMenuShortcut = (callback: () => void) => {
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
callback();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [callback]);
};
export {
CommandMenu,
CommandMenuTrigger,
CommandMenuContent,
CommandMenuTitle,
CommandMenuDescription,
CommandMenuInput,
CommandMenuList,
CommandMenuEmpty,
CommandMenuGroup,
CommandMenuItem,
CommandMenuSeparator,
CommandMenuClose,
useCommandMenu,
};
Use the CLI to add the component to your project:
npx hextaui@latest@latest add command-menu
pnpm dlx hextaui@latest@latest add command-menu
yarn dlx hextaui@latest@latest add command-menu
bun x hextaui@latest@latest add command-menu
Usage
import * as React from "react";
import {
CommandMenu,
CommandMenuTrigger,
CommandMenuContent,
CommandMenuInput,
CommandMenuList,
CommandMenuGroup,
CommandMenuItem,
useCommandMenuShortcut,
} from "@/components/ui/command-menu";
import { Button } from "@/components/ui/button";
import { Search, Settings, User } from "lucide-react";
export const BasicCommandMenu = () => {
const [open, setOpen] = React.useState(false);
useCommandMenuShortcut(() => setOpen(true));
return (
<CommandMenu open={open} onOpenChange={setOpen}>
<CommandMenuTrigger asChild>
<Button variant="outline">
Open Command Menu <kbd>⌘K</kbd>
</Button>
</CommandMenuTrigger>
<CommandMenuContent>
<CommandMenuInput placeholder="Type a command or search..." />
<CommandMenuList>
<CommandMenuGroup heading="Suggestions">
<CommandMenuItem icon={<Search />} shortcut="⌘+F">
Search
</CommandMenuItem>
<CommandMenuItem icon={<Settings />} shortcut="⌘+,">
Settings
</CommandMenuItem>
<CommandMenuItem icon={<User />} shortcut="⌘+U">
Profile
</CommandMenuItem>
</CommandMenuGroup>
</CommandMenuList>
</CommandMenuContent>
</CommandMenu>
);
};
OS Detection and Keyboard Shortcuts
The component automatically detects the user's operating system and displays the appropriate modifier keys:
- macOS: Shows ⌘ (Command key)
- Windows/Linux: Shows Ctrl
Keyboard shortcuts in CommandMenuItem
components support the following format:
- Use
cmd
for the main modifier key (automatically converted to ⌘ on macOS or Ctrl on Windows/Linux) - Use
shift
,alt
,ctrl
for other modifiers - Separate multiple keys with
+
(e.g.,"cmd+shift+s"
)
Basic Usage
import * as React from "react";
import {
CommandMenu,
CommandMenuTrigger,
CommandMenuContent,
CommandMenuInput,
CommandMenuList,
CommandMenuGroup,
CommandMenuItem,
useCommandMenuShortcut,
} from "@/components/ui/command-menu";
import { Button } from "@/components/ui/button";
import { Search, Settings, User } from "lucide-react";
export const BasicCommandMenu = () => {
const [open, setOpen] = React.useState(false);
useCommandMenuShortcut(() => setOpen(true));
return (
<CommandMenu open={open} onOpenChange={setOpen}>
<CommandMenuTrigger asChild>
<Button variant="outline">
Open Command Menu <kbd>⌘K</kbd>
</Button>
</CommandMenuTrigger>
<CommandMenuContent>
<CommandMenuInput placeholder="Type a command or search..." />
<CommandMenuList>
<CommandMenuGroup heading="Suggestions">
<CommandMenuItem icon={<Search />} shortcut="⌘+F">
Search
</CommandMenuItem>
<CommandMenuItem icon={<Settings />} shortcut="⌘+,">
Settings
</CommandMenuItem>
<CommandMenuItem icon={<User />} shortcut="⌘+U">
Profile
</CommandMenuItem>
</CommandMenuGroup>
</CommandMenuList>
</CommandMenuContent>
</CommandMenu>
);
};
With Search and Filtering
const [open, setOpen] = React.useState(false);
const [searchValue, setSearchValue] = React.useState("");
const searchResults = [
{ id: 1, name: "Dashboard", type: "page", icon: <Home /> },
{ id: 2, name: "User Profile", type: "page", icon: <User /> },
{ id: 3, name: "Settings", type: "page", icon: <Settings /> },
{ id: 4, name: "John Doe", type: "user", icon: <User /> },
{ id: 5, name: "Jane Smith", type: "user", icon: <User /> },
{ id: 6, name: "Project Alpha", type: "project", icon: <FileText /> },
{ id: 7, name: "Team Meeting Notes", type: "document", icon: <FileText /> },
];
const filteredResults = searchResults.filter((item) =>
item.name.toLowerCase().includes(searchValue.toLowerCase())
);
return (
<CommandMenu open={open} onOpenChange={setOpen}>
<CommandMenuTrigger asChild>
<Button variant="ghost" className="gap-2">
<Search size={16} />
Search Everything
</Button>
</CommandMenuTrigger>
<CommandMenuContent>
<CommandMenuInput
placeholder="Search pages, users, projects..."
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
/>
<CommandMenuList>
{filteredResults.length === 0 ? (
<CommandMenuEmpty>
No results found for "{searchValue}"
</CommandMenuEmpty>
) : (
<>
<CommandMenuGroup heading="Results">
{filteredResults.map((item, index) => (
<CommandMenuItem
key={item.id}
icon={item.icon}
index={index}
onSelect={() => {
console.log(`Selected: ${item.name}`);
setOpen(false);
}}
>
<div className="flex flex-col">
<span>{item.name}</span>
<span className="text-xs text-[hsl(var(--hu-muted-foreground))] capitalize">
{item.type}
</span>
</div>
</CommandMenuItem>
))}
</CommandMenuGroup>
</>
)}
</CommandMenuList>
</CommandMenuContent>
</CommandMenu>
);
};
</CommandMenuContent>
</CommandMenu>
);
};
Action Commands
const [open, setOpen] = React.useState(false);
return (
<CommandMenu open={open} onOpenChange={setOpen}>
<CommandMenuTrigger asChild>
<Button variant="outline">Quick Actions</Button>
</CommandMenuTrigger>
<CommandMenuContent>
<CommandMenuInput placeholder="Choose an action..." />
<CommandMenuList>
<CommandMenuGroup heading="File Actions">
<CommandMenuItem icon={<Plus />} index={0} shortcut="cmd+n">
New File
</CommandMenuItem>
<CommandMenuItem icon={<Upload />} index={1} shortcut="cmd+u">
Upload File
</CommandMenuItem>
<CommandMenuItem icon={<Download />} index={2} shortcut="cmd+d">
Download All
</CommandMenuItem>
<CommandMenuItem icon={<Copy />} index={3} shortcut="cmd+c">
Copy Link
</CommandMenuItem>
</CommandMenuGroup>
<CommandMenuSeparator />
<CommandMenuGroup heading="Edit Actions">
<CommandMenuItem icon={<Edit />} index={4} shortcut="cmd+e">
Edit Document
</CommandMenuItem>
<CommandMenuItem icon={<RotateCcw />} index={5} shortcut="cmd+z">
Undo Changes
</CommandMenuItem>
<CommandMenuItem icon={<Archive />} index={6}>
Archive Item
</CommandMenuItem>
<CommandMenuItem icon={<Trash2 />} index={7} shortcut="del">
Delete Item
</CommandMenuItem>
</CommandMenuGroup>
<CommandMenuSeparator />
<CommandMenuGroup heading="Share & Export">
<CommandMenuItem icon={<Share />} index={8} shortcut="cmd+shift+s">
Share
</CommandMenuItem>
<CommandMenuItem icon={<ExternalLink />} index={9}>
Open in New Tab
</CommandMenuItem>
<CommandMenuItem icon={<Bookmark />} index={10} shortcut="cmd+b">
Bookmark
</CommandMenuItem>
</CommandMenuGroup>
</CommandMenuList>
</CommandMenuContent>
</CommandMenu>
);
};
Navigation Menu
const [open, setOpen] = React.useState(false);
useCommandMenuShortcut(() => setOpen(true));
const navigationItems = [
{ name: "Dashboard", href: "/dashboard", icon: <Home />, shortcut: "g+d" },
{
name: "Calendar",
href: "/calendar",
icon: <Calendar />,
shortcut: "g+c",
},
{ name: "Messages", href: "/messages", icon: <Mail />, shortcut: "g+m" },
{
name: "Documents",
href: "/documents",
icon: <FileText />,
shortcut: "g+f",
},
{
name: "Settings",
href: "/settings",
icon: <Settings />,
shortcut: "g+s",
},
{ name: "Profile", href: "/profile", icon: <User />, shortcut: "g+p" },
];
return (
<CommandMenu open={open} onOpenChange={setOpen}>
<CommandMenuTrigger asChild>
<Button variant="ghost" size="sm" className="gap-2">
<Command size={14} />
Navigate
</Button>
</CommandMenuTrigger>
<CommandMenuContent>
<CommandMenuInput placeholder="Where would you like to go?" />
<CommandMenuList>
<CommandMenuGroup heading="Quick Navigation">
{navigationItems.map((item, index) => (
<CommandMenuItem
key={item.name}
icon={item.icon}
index={index}
shortcut={item.shortcut}
onSelect={() => {
console.log(`Navigating to: ${item.href}`);
setOpen(false);
}}
>
{item.name}
</CommandMenuItem>
))}
</CommandMenuGroup>
<CommandMenuSeparator />
<CommandMenuGroup heading="Recent">
<CommandMenuItem icon={<Clock />} index={6}>
Recently Viewed
</CommandMenuItem>
<CommandMenuItem icon={<Star />} index={7}>
Favorites
</CommandMenuItem>
<CommandMenuItem icon={<Heart />} index={8}>
Bookmarked Pages
</CommandMenuItem>
</CommandMenuGroup>
</CommandMenuList>
</CommandMenuContent>
</CommandMenu>
);
};
Complex Command Menu
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState("");
useCommandMenuShortcut(() => setOpen(true));
const allItems = [
// Pages
{ type: "page", name: "Dashboard", icon: <Home />, shortcut: "g+d" },
{ type: "page", name: "Analytics", icon: <Settings />, shortcut: "g+a" },
{ type: "page", name: "Calendar", icon: <Calendar />, shortcut: "g+c" },
// Actions
{
type: "action",
name: "Create New Project",
icon: <Plus />,
shortcut: "cmd+n",
},
{
type: "action",
name: "Upload Files",
icon: <Upload />,
shortcut: "cmd+u",
},
{
type: "action",
name: "Export Data",
icon: <Download />,
shortcut: "cmd+e",
},
// Users
{ type: "user", name: "John Doe", icon: <User /> },
{ type: "user", name: "Jane Smith", icon: <User /> },
{ type: "user", name: "Mike Johnson", icon: <User /> },
// Documents
{ type: "document", name: "Project Proposal.pdf", icon: <FileText /> },
{ type: "document", name: "Meeting Notes", icon: <FileText /> },
{ type: "document", name: "Budget Spreadsheet", icon: <FileText /> },
];
const filteredItems = React.useMemo(() => {
if (!value) return allItems;
return allItems.filter(
(item) =>
item.name.toLowerCase().includes(value.toLowerCase()) ||
item.type.toLowerCase().includes(value.toLowerCase())
);
}, [value]);
const groupedItems = React.useMemo(() => {
const groups: Record<string, typeof allItems> = {};
filteredItems.forEach((item) => {
if (!groups[item.type]) groups[item.type] = [];
groups[item.type].push(item);
});
return groups;
}, [filteredItems]);
const getGroupTitle = (type: string) => {
switch (type) {
case "page":
return "Pages";
case "action":
return "Actions";
case "user":
return "Users";
case "document":
return "Documents";
default:
return type;
}
};
let globalIndex = 0;
return (
<CommandMenu open={open} onOpenChange={setOpen}>
<CommandMenuTrigger asChild>
<Button className="gap-2">
<Search size={16} />
Command Palette
<kbd className="pointer-events-none h-5 select-none items-center gap-1 rounded border border-[hsl(var(--hu-border))] bg-[hsl(var(--hu-muted))] px-1.5 font-mono text-[10px] font-medium opacity-100 ml-auto flex">
⌘K
</kbd>
</Button>
</CommandMenuTrigger>
<CommandMenuContent>
<CommandMenuInput
placeholder="Type to search pages, actions, users, documents..."
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<CommandMenuList maxHeight="400px">
{Object.keys(groupedItems).length === 0 ? (
<CommandMenuEmpty>No results found for "{value}"</CommandMenuEmpty>
) : (
Object.entries(groupedItems).map(([type, items], groupIndex) => (
<React.Fragment key={type}>
{groupIndex > 0 && <CommandMenuSeparator />}
<CommandMenuGroup heading={getGroupTitle(type)}>
{items.map((item, index) => {
const currentIndex = globalIndex++;
return (
<CommandMenuItem
key={`${type}-${index}`}
icon={item.icon}
index={currentIndex}
shortcut={item.shortcut}
onSelect={() => {
console.log(`Selected ${type}: ${item.name}`);
setOpen(false);
setValue("");
}}
>
{item.name}
</CommandMenuItem>
);
})}
</CommandMenuGroup>
</React.Fragment>
))
)}
</CommandMenuList>
</CommandMenuContent>
</CommandMenu>
);
};
);
};
Minimal Command Menu
const [open, setOpen] = React.useState(false);
return (
<CommandMenu open={open} onOpenChange={setOpen}>
<CommandMenuTrigger asChild>
<Button variant="ghost" size="sm">
<Command size={16} />
</Button>
</CommandMenuTrigger>
<CommandMenuContent showShortcut={false}>
<CommandMenuInput placeholder="Quick search..." />
<CommandMenuList>
<CommandMenuItem icon={<Home />} index={0}>
Home
</CommandMenuItem>
<CommandMenuItem icon={<Settings />} index={1}>
Settings
</CommandMenuItem>
<CommandMenuItem icon={<User />} index={2}>
Profile
</CommandMenuItem>
</CommandMenuList>
</CommandMenuContent>
</CommandMenu>
);
};
Keyboard Shortcuts
⌘K
(orCtrl+K
): Open command menu (when usinguseCommandMenuShortcut
)↑/↓
: Navigate through command itemsEnter
: Execute selected commandEscape
: Close command menuTab
: Navigate through focusable elements
Props
CommandMenu
Prop | Type | Default |
---|---|---|
onOpenChange? | (open: boolean) => void | undefined |
open? | boolean | false |
CommandMenuContent
Prop | Type | Default |
---|---|---|
className? | string | undefined |
showShortcut? | boolean | true |
CommandMenuTitle
Prop | Type | Default |
---|---|---|
className? | string | undefined |
CommandMenuDescription
Prop | Type | Default |
---|---|---|
className? | string | undefined |
CommandMenuInput
Prop | Type | Default |
---|---|---|
className? | string | undefined |
placeholder? | string | "Type a command or search..." |
CommandMenuList
Prop | Type | Default |
---|---|---|
className? | string | undefined |
maxHeight? | string | "300px" |
CommandMenuGroup
Prop | Type | Default |
---|---|---|
className? | string | undefined |
heading? | string | undefined |
CommandMenuItem
Prop | Type | Default |
---|---|---|
className? | string | undefined |
index? | number | 0 |
icon? | ReactNode | undefined |
shortcut? | string | undefined |
disabled? | boolean | false |
onSelect? | () => void | undefined |
CommandMenuEmpty
Prop | Type | Default |
---|---|---|
className? | string | undefined |
children? | ReactNode | "No results found." |
useCommandMenuShortcut
Prop | Type | Default |
---|---|---|
callback? | () => void | undefined |
Edit on GitHub
Last updated on