Build websites 10x faster with HextaUI Blocks — Learn more
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.

components/ui/command-menu.tsx
"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>
  );
};
  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 (or Ctrl+K): Open command menu (when using useCommandMenuShortcut)
  • ↑/↓: Navigate through command items
  • Enter: Execute selected command
  • Escape: Close command menu
  • Tab: Navigate through focusable elements

Props

CommandMenu

PropTypeDefault
onOpenChange?
(open: boolean) => void
undefined
open?
boolean
false

CommandMenuContent

PropTypeDefault
className?
string
undefined
showShortcut?
boolean
true

CommandMenuTitle

PropTypeDefault
className?
string
undefined

CommandMenuDescription

PropTypeDefault
className?
string
undefined

CommandMenuInput

PropTypeDefault
className?
string
undefined
placeholder?
string
"Type a command or search..."

CommandMenuList

PropTypeDefault
className?
string
undefined
maxHeight?
string
"300px"

CommandMenuGroup

PropTypeDefault
className?
string
undefined
heading?
string
undefined

CommandMenuItem

PropTypeDefault
className?
string
undefined
index?
number
0
icon?
ReactNode
undefined
shortcut?
string
undefined
disabled?
boolean
false
onSelect?
() => void
undefined

CommandMenuEmpty

PropTypeDefault
className?
string
undefined
children?
ReactNode
"No results found."

useCommandMenuShortcut

PropTypeDefault
callback?
() => void
undefined
Edit on GitHub

Last updated on