Command
A cmdk-powered palette with fuzzy search, groups, items and shortcuts, inline or as a ⌘K dialog.
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandShortcut,
} from "@/components/ui/command"
export default function CommandDemo() {
return (
<div className="max-w-md rounded-lg border border-border">
<Command>
<CommandInput placeholder="Type a command or search…" />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
<CommandItem>
New deployment <CommandShortcut>⌘N</CommandShortcut>
</CommandItem>
<CommandItem>Search docs</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</div>
)
}Installation
Run the following command
npx shadcn@latest add @ai2/commandDependencies, the @ai2/tokens theme and the component file are installed together. The @ai2/dialog component installs alongside, since CommandDialog is built on it.
Install dependencies
npm install cmdk@^1.1.1 lucide-react@^1.23.0Add the cn util
lib/utils.tsimport { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/* Adds a source-attribution ref param to a URL (the inspiration exports mark their
outbound links with an ai2.design attribution). An invalid URL is returned as is.
This file is SHOWN TO THE CONSUMER: the docs component pages render the source of
`cn` in a code block, so a Turkish comment here would reach every one of those
pages. Keep it English. */
export function withRef(url: string, ref = "ai2.design"): string {
try {
const u = new URL(url)
u.searchParams.set("ref", ref)
return u.toString()
} catch {
return url
}
}Copy the source code
components/ui/command.tsx"use client"
import type * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "@/lib/utils"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-lg bg-popover text-popover-foreground",
className
)}
{...props}
/>
)
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run…",
children,
className,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string
description?: string
className?: string
}) {
return (
<Dialog {...props}>
<DialogContent className={cn("overflow-hidden p-0", className)} showClose={false}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-input-wrapper]_svg]:size-4 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-2">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div data-slot="command-input-wrapper" className="flex items-center gap-2 border-b border-border px-3">
<Search className="size-4 shrink-0 text-muted-foreground" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"flex h-10 w-full bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
)
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn("max-h-80 scroll-py-1 overflow-y-auto overflow-x-hidden p-1", className)}
{...props}
/>
)
}
function CommandEmpty(props: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm text-muted-foreground"
{...props}
/>
)
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn("overflow-hidden text-foreground", className)}
{...props}
/>
)
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none transition-colors duration-(--motion-fast) data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground [&_i]:shrink-0 [&_i]:text-base [&_i]:leading-none [&_i]:text-muted-foreground",
className
)}
{...props}
/>
)
}
function CommandShortcut({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
{...props}
/>
)
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}Command imports the ai2 dialog, so copy components/ui/dialog.tsx as well. Manual installs skip the @ai2/tokens theme, so add the token CSS from the theming guide or the tone colors will be missing.
Usage
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
<Command>
<CommandInput placeholder="Type a command or search…" />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
<CommandItem>New deployment</CommandItem>
<CommandItem>Search docs</CommandItem>
</CommandGroup>
</CommandList>
</Command>Typing in CommandInput fuzzy-filters the items automatically; CommandEmpty shows when nothing matches. A plain Command works inline (e.g. inside a popover); use CommandDialog for a global palette.
Examples
Groups and separators
Command dialog
For a global ⌘K palette, keep open in state and toggle it from a keydown listener on the document.
Props
Every part wraps the matching cmdk primitive and accepts its props: heading on groups, value, disabled and onSelect on items. The ai2-specific props live on CommandDialog.
CommandDialog props
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | "Command Palette" | Screen-reader-only dialog title. |
description | string | "Search for a command to run…" | Screen-reader-only dialog description. |
className | string | - | Extra classes for the dialog content wrapper. |
CommandDialog also accepts every Dialog prop, e.g. open and onOpenChange.
ai2 Command: a cmdk command palette for React
The ai2 Command is a shadcn-compatible command palette component for React, built on the cmdk library and styled with Tailwind CSS v4. It gives your product the Ctrl+K / Cmd+K pattern users know from editors and dashboards: type a few characters, fuzzy search filters actions, pages and settings instantly, and Enter runs the selected one.
Because it ships through the shadcn registry format, you install it with one CLI command, an MCP agent, or a copy-paste, and the source lands in your own project. You own the file; there is no runtime dependency on ai2 itself. The live example above is the exact component you get.
What is the ai2 Command?
It is a nine-part composition: Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut and CommandSeparator. The anatomy matches shadcn/ui exactly, so existing snippets and AI agents keep working without changes.
Every part wraps the matching cmdk primitive, which does the heavy lifting: fuzzy filtering, item scoring, keyboard navigation and selection state. A plain Command renders inline, inside a popover or a sidebar; CommandDialog wraps the same list in the ai2 Dialog for a global palette.
Why use it
- Fuzzy search for free: cmdk filters and ranks items as the user types. No search index, no debouncing, no filtering code on your side.
- The Ctrl+K / Cmd+K pattern: CommandDialog plus one document keydown listener gives you the global palette users expect from modern tools like editors and dashboards.
- Keyboard-first interaction: Arrow keys move the selection, Enter fires the item's onSelect, Escape closes the dialog. Focus stays in the input the whole time, so users never stop typing.
- Screen reader wiring included: CommandDialog renders a visually hidden DialogTitle and DialogDescription by default, so the dialog announces correctly without extra work.
- Agent-readable metadata: The registry item describes its parts and intended use in plain words, so an MCP agent can find, inspect and install it without guessing.
Features
- shadcn registry install: One command adds the component, the cmdk dependency, the ai2 dialog it builds on and the @ai2/tokens theme.
- CommandDialog wrapper: A ready-made modal palette: ai2 Dialog with padding stripped, the close button hidden and sr-only title and description props.
- Groups, separators and shortcuts: CommandGroup takes a heading, CommandSeparator splits sections, and CommandShortcut right-aligns a keyboard hint inside any item.
- Empty state built in: CommandEmpty renders automatically when the query matches nothing, so the zero-results case is one line of JSX.
- Motion from tokens: Item highlight transitions run on --motion-fast from the shared token file, matching every other ai2 component.
- Data attributes for styling: Every part exposes data-slot, and cmdk adds data-selected and data-disabled, so you can restyle states from CSS without forking the component.
Production tips
- Register the shortcut once: Add a single document keydown listener for Ctrl+K / Cmd+K in your root layout and toggle the CommandDialog open state from it. Remember to preventDefault so the browser shortcut does not fire.
- Close after running an action: Call setOpen(false) inside each item's onSelect. A palette that stays open after the action ran feels broken.
- Give items stable values: cmdk matches against the item's text by default. If two items share text or the label is dynamic, set an explicit value prop so filtering and selection stay predictable.
- Group by intent, not by module: Headings like Navigation, Actions and Settings help users scan. Mirroring your internal code structure usually does not.
- Keep shortcuts honest: CommandShortcut only renders the hint text. Only show a shortcut when a real global handler exists for it.
Works with the rest of ai2
Command is the engine behind other pickers: the ai2 Combobox mounts this exact list inside an ai2 Popover for searchable selects, and CommandDialog is built on the ai2 Dialog.
Inside items, use the ai2 Kbd component to render keycap-styled shortcut hints, and trigger the palette from an ai2 Button in the header for users who do not know the keyboard shortcut. Everything shares one token source, so the palette matches the rest of your UI in both modes.