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

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";
import { ScrollArea } from "@/components/ui/scroll-area";

// 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;
  scrollType?: "auto" | "always" | "scroll" | "hover";
  scrollHideDelay?: number;
}

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;
  scrollType?: "auto" | "always" | "scroll" | "hover";
  scrollHideDelay?: number;
}> = ({
  children,
  value,
  setValue,
  selectedIndex,
  setSelectedIndex,
  scrollType,
  scrollHideDelay,
}) => (
  <CommandMenuContext.Provider
    value={{
      value,
      setValue,
      selectedIndex,
      setSelectedIndex,
      scrollType,
      scrollHideDelay,
    }}
  >
    {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-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-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;
    scrollType?: "auto" | "always" | "scroll" | "hover";
    scrollHideDelay?: number;
  }
>(
  (
    {
      className,
      children,
      showShortcut = true,
      scrollType = "hover",
      scrollHideDelay = 600,
      ...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-background border border-border rounded-card shadow-lg",
              "overflow-hidden",
              className
            )}
          >
            {" "}
            <CommandMenuProvider
              value={value}
              setValue={setValue}
              selectedIndex={selectedIndex}
              setSelectedIndex={setSelectedIndex}
              scrollType={scrollType}
              scrollHideDelay={scrollHideDelay}
            >
              <VisuallyHidden.Root>
                <CommandMenuTitle>Command Menu</CommandMenuTitle>
              </VisuallyHidden.Root>

              {children}

              <CommandMenuClose className="absolute right-3 top-3 rounded-lg p-1.5 text-muted-foreground hover:text-foreground hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-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-border px-3 py-0">
        <Search className="mr-3 h-4 w-4 shrink-0 text-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-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
            className
          )}
          placeholder={placeholder}
          {...props}
        />
      </div>
    );
  }
);
CommandMenuInput.displayName = "CommandMenuInput";

// List container for command items with scroll area
const CommandMenuList = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement> & {
    maxHeight?: string;
  }
>(({ className, children, maxHeight = "300px", ...props }, ref) => {
  const {
    selectedIndex,
    setSelectedIndex,
    scrollType = "hover",
    scrollHideDelay = 600,
  } = 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();
        const newIndex = Math.min(selectedIndex + 1, maxIndex);
        setSelectedIndex(newIndex);

        // Scroll selected item into view
        const selectedItem = items[newIndex] as HTMLElement;
        if (selectedItem) {
          selectedItem.scrollIntoView({
            block: "nearest",
            behavior: "smooth",
          });
        }
      } else if (e.key === "ArrowUp") {
        e.preventDefault();
        const newIndex = Math.max(selectedIndex - 1, 0);
        setSelectedIndex(newIndex);

        // Scroll selected item into view
        const selectedItem = items[newIndex] as HTMLElement;
        if (selectedItem) {
          selectedItem.scrollIntoView({
            block: "nearest",
            behavior: "smooth",
          });
        }
      }
    };

    document.addEventListener("keydown", handleKeyDown);
    return () => document.removeEventListener("keydown", handleKeyDown);
  }, [selectedIndex, setSelectedIndex]);

  return (
    <div ref={ref} className="p-1" {...props}>
      <ScrollArea
        className={cn("w-full", className)}
        style={{ height: maxHeight }}
        type={scrollType}
        scrollHideDelay={scrollHideDelay}
      >
        <div className="space-y-1 p-1">{children}</div>
      </ScrollArea>
    </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-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-ele px-2 py-2 text-sm outline-none transition-colors gap-2",
          "hover:bg-accent hover:text-accent-foreground",
          isSelected &&
            "bg-accent text-accent-foreground",
          disabled && "pointer-events-none opacity-50",
          className
        )}
        onClick={handleSelect}
        onMouseEnter={() => setSelectedIndex(index)}
        {...props}
      >
        {icon && (
          <div className="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-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-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-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

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-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-border bg-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
open?
boolean
false
onOpenChange?
(open: boolean) => void
undefined

CommandMenuContent

PropTypeDefault
showShortcut?
boolean
true
className?
string
undefined

CommandMenuTitle

PropTypeDefault
className?
string
undefined

CommandMenuDescription

PropTypeDefault
className?
string
undefined

CommandMenuInput

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

CommandMenuList

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

CommandMenuGroup

PropTypeDefault
heading?
string
undefined
className?
string
undefined

CommandMenuItem

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

CommandMenuEmpty

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

useCommandMenuShortcut

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

Last updated on