Navigation

Command Palette

A ⌘K command palette with live fuzzy search, grouped commands, and keyboard navigation, its highlight row sliding via a shared layout.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add command-palette.en

New project or installation trouble? Start here →

Command Palette is a React ⌘K command menu built with Tailwind CSS and Motion. Press ⌘K or Ctrl+K to open it, type to filter commands instantly, and run one with the arrow keys and Enter, without an extra combobox library.

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

Active edited usageDefault props
<CommandPalette/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

When to use

  • Global quick navigation in a SaaS dashboard, such as "Go to billing" or "Open settings".
  • A fast search entry point for a docs site or knowledge base that lists common pages.
  • Collecting actions scattered across menus in an editor or dashboard into one keyboard entry point.
  • Frequent switches in an admin UI, like changing workspace, project or theme.

Installation

npx webberui@latest add https://webberui.com/r/command-palette.en.json

Or, once registries are configured in components.json, install it as @webberui/command-palette.

Usage

import * as React from "react";
import { Home, Plus } from "lucide-react";
import { CommandPalette } from "@/components/ui/command-palette";

export function Example() {
  const [open, setOpen] = React.useState(false);

  const groups = [
    {
      heading: "Navigation",
      items: [
        {
          id: "home",
          label: "Go to home",
          icon: <Home className="size-4" />,
          shortcut: "G H",
          onSelect: () => console.log("home"),
        },
      ],
    },
    {
      heading: "Actions",
      items: [
        {
          id: "new",
          label: "New document",
          icon: <Plus className="size-4" />,
          shortcut: "⌘ N",
          onSelect: () => console.log("new"),
        },
      ],
    },
  ];

  return <CommandPalette open={open} onOpenChange={setOpen} groups={groups} />;
}

Press ⌘K (macOS) or Ctrl+K (Windows/Linux) to summon the palette at any time; you can also drive it from an external button through open / onOpenChange.

Examples

Call router.push inside onSelect; the palette closes first and then runs the action. A button covers users who do not know the shortcut.

"use client";

import * as React from "react";
import { useRouter } from "next/navigation";
import { CreditCard, LayoutDashboard, Settings } from "lucide-react";
import { CommandPalette } from "@/components/ui/command-palette";

export function AppCommandMenu() {
  const router = useRouter();
  const [open, setOpen] = React.useState(false);

  const groups = [
    {
      heading: "Pages",
      items: [
        { id: "dashboard", label: "Dashboard", icon: <LayoutDashboard className="size-4" />, onSelect: () => router.push("/dashboard") },
        { id: "billing", label: "Billing and payments", icon: <CreditCard className="size-4" />, onSelect: () => router.push("/settings/billing") },
        { id: "settings", label: "Account settings", icon: <Settings className="size-4" />, shortcut: "⌘ ,", onSelect: () => router.push("/settings") },
      ],
    },
  ];

  return (
    <>
      <button onClick={() => setOpen(true)}>Search... ⌘K</button>
      <CommandPalette open={open} onOpenChange={setOpen} groups={groups} placeholder="Search pages or commands..." />
    </>
  );
}

Uncontrolled mode

Without open, the component manages its own state and opens only with ⌘K / Ctrl+K, which suits dropping it into the root layout.

<CommandPalette
  groups={[
    {
      heading: "Appearance",
      items: [
        { id: "light", label: "Switch to light theme", onSelect: () => setTheme("light") },
        { id: "dark", label: "Switch to dark theme", onSelect: () => setTheme("dark") },
      ],
    },
  ]}
/>

Build groups from data

groups is a plain array, so you can build it from an API response or props, for example recently opened projects.

const groups = [
  {
    heading: "Recent projects",
    items: recentProjects.map((project) => ({
      id: `project-${project.id}`,
      label: project.name,
      onSelect: () => router.push(`/projects/${project.id}`),
    })),
  },
];

Props

PropTypeDefaultDescription
openboolean—Open state in controlled mode; when omitted, the component manages it internally
onOpenChange(open: boolean) => void—Callback fired when the open state changes (in both controlled and uncontrolled modes)
groupsCommandPaletteGroup[]—Command group data; each group's heading and items are rendered in order
placeholderstring"Type a command or search…"Placeholder text for the search field
classNamestring—Appended to the palette container's className

CommandPaletteGroup

FieldTypeDescription
headingstringGroup heading
itemsCommandPaletteItem[]The command items in that group

CommandPaletteItem

FieldTypeDescription
idstringUnique identifier, used as the option's DOM id and the React key
labelstringDisplay text, which is also what search filtering matches against
iconReact.ReactNodeOptional leading icon
shortcutstringOptional keyboard shortcut hint, shown on the right of the item
onSelect() => voidThe action to run when this item is selected (by click or Enter)

How it works

  • A global listener toggles on ⌘K / Ctrl+K; the palette is mounted onto document.body with createPortal.
  • The overlay fades in and the palette scales and fades in, centered slightly above the middle; the search field focuses automatically and typing applies live subsequence fuzzy filtering.
  • The up and down keys cycle the highlight through the filtered items, Enter runs the item, Esc closes, and clicking the overlay closes.
  • The highlight row's background slides smoothly between items via a shared layoutId.

Accessibility

  • The palette is role="dialog" with aria-modal="true"; it locks body scrolling while open and returns focus to the previously focused element after closing.
  • The search field is role="combobox" and points at the currently highlighted item with aria-activedescendant; the results are role="listbox" and the items are role="option" with aria-selected.
  • When the user has "reduce motion" enabled at the system level, the palette only fades in and out by opacity and the highlight row snaps into place, with no scaling or sliding.

FAQ

Does it work with the Next.js App Router?

Yes. The source starts with "use client". The panel renders through createPortal into document.body only after mount, so the server output is empty and there is no hydration mismatch.

How does the search match?

It matches only each item's label, using a case-insensitive subsequence match: the query characters must appear in order, not necessarily next to each other. Groups with no matches are hidden, and an empty-state message shows when nothing matches.

Does the palette close after selecting an item?

Yes. On click or Enter, the component calls onOpenChange(false) first and then runs the item's onSelect.

How well does it support keyboards and screen readers?

Up and Down move the highlight with wraparound, Enter runs, Esc closes. The panel is a role="dialog" that locks body scroll, the input is a role="combobox" pointing at the highlighted item with aria-activedescendant, and focus returns to the previously focused element on close.

Do I need cmdk or a similar package?

No. Dependencies are only motion, lucide-react, clsx and tailwind-merge; filtering and keyboard navigation live in the component source, which you can copy from the Manual tab.