Command

A searchable list of actions, on the page or in a dialog.

Suggestions
Calendar
Search Emoji
Calculator
Settings
Profile⌘P
Billing⌘B
Settings⌘S
import { Fragment } from "react"
import * as stylex from "@stylexjs/stylex"
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
  CommandShortcut,
} from "@/components/ui/command"
import { radius } from "@/theme/tokens.stylex"
import { Calculator, Calendar, CreditCard, Settings, Smile, User } from "lucide-react"
import type { LucideIcon } from "lucide-react"

type Action = {
  value: string
  label: string
  icon: LucideIcon
  shortcut?: string
  disabled?: boolean
}
type Group = { value: string; items: Action[] }

const groups: Group[] = [
  {
    value: "Suggestions",
    items: [
      { value: "calendar", label: "Calendar", icon: Calendar },
      { value: "emoji", label: "Search Emoji", icon: Smile },
      { value: "calculator", label: "Calculator", icon: Calculator, disabled: true },
    ],
  },
  {
    value: "Settings",
    items: [
      { value: "profile", label: "Profile", icon: User, shortcut: "⌘P" },
      { value: "billing", label: "Billing", icon: CreditCard, shortcut: "⌘B" },
      { value: "settings", label: "Settings", icon: Settings, shortcut: "⌘S" },
    ],
  },
]

const styles = stylex.create({
  command: { maxWidth: "24rem", borderRadius: radius.lg, borderWidth: 1, borderStyle: "solid" },
})

export default function CommandDemo() {
  return (
    <Command items={groups} xstyle={styles.command}>
      <CommandInput placeholder="Type a command or search..." aria-label="Search commands" />
      <CommandEmpty>No results found.</CommandEmpty>
      <CommandList>
        {(group: Group, index: number) => (
          <Fragment key={group.value}>
            {index > 0 && <CommandSeparator />}
            <CommandGroup heading={group.value} items={group.items}>
              {(item: Action) => (
                <CommandItem key={item.value} value={item} disabled={item.disabled}>
                  <item.icon />
                  <span>{item.label}</span>
                  {item.shortcut && <CommandShortcut>{item.shortcut}</CommandShortcut>}
                </CommandItem>
              )}
            </CommandGroup>
          </Fragment>
        )}
      </CommandList>
    </Command>
  )
}

About

Command is Base UI's Autocomplete rendered inline, not the cmdk package that shadcn's Command is built on. The list is always open, the input filters it, and an item runs its onClick when clicked or when Enter is pressed while it is highlighted. The trade-off is the data shape. Options go through items and render from a function child, the way a Combobox does.

Installation

Install the following dependencies:

pnpm add @base-ui/react @stylexjs/stylex clsx lucide-react

Set up the token layer and the StyleX compiler, if you have not already. See Installation, and paste states.ts and state-styles.ts from the same page. states.ts holds the dimmed item style, and an item reads its highlight and disabled state through state-styles.ts. The list needs interop.css for its hidden scrollbar and for the icons it does not render.

Add Input, Input Group and Dialog. The search field is an InputGroup, and CommandDialog is a Dialog.

Copy and paste the following code into your project.

components/ui/command.tsx
"use client"

import { useRender } from "@base-ui/react/use-render"
import type { ComponentProps, CSSProperties, ReactNode } from "react"
import { Autocomplete as AutocompletePrimitive } from "@base-ui/react/autocomplete"
import * as stylex from "@stylexjs/stylex"
import type { StyleXStyles } from "@stylexjs/stylex"
import { clsx } from "clsx"
import { Search } from "lucide-react"

import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { InputGroup, InputGroupAddon } from "@/components/ui/input-group"
import { stateStyles } from "@/lib/state-styles"
import { states } from "@/theme/states"
import { color, leading, radius, space, text, tracking } from "@/theme/tokens.stylex"

const styles = stylex.create({
  root: {
    display: "flex",
    width: "100%",
    height: "100%",
    flexDirection: "column",
    overflow: "hidden",
    "--carbon-command-radius": radius.xl,
    borderRadius: "var(--carbon-command-radius)",
    padding: space.s1,
    backgroundColor: color.popover,
    color: color.popoverForeground,
  },
  inputWrapper: {
    marginInline: `calc(-1 * ${space.s1})`,
    marginBlockStart: `calc(-1 * ${space.s1})`,
    paddingInline: space.s1,
    borderBlockEndWidth: 1,
    borderBlockEndStyle: "solid",
    borderBlockEndColor: color.border,
  },
  inputFrame: {
    minHeight: space.s10,
    borderColor: color.transparent,
    backgroundColor: color.transparent,
    boxShadow: "none",
  },
  list: {
    maxHeight: "18rem",
    overflowX: "hidden",
    overflowY: "auto",
    overscrollBehavior: "contain",
    scrollPaddingTop: space.s1,
    scrollPaddingBottom: space.s1,
    outlineStyle: "none",
  },
  empty: {
    paddingBlock: { default: space.s6, ":empty": 0 },
    textAlign: "center",
    fontSize: text.sm,
    lineHeight: leading.sm,
  },
  group: { overflow: "hidden", padding: space.s1, color: color.foreground },
  heading: {
    paddingInline: space.s2,
    paddingBlock: space.s1_5,
    fontSize: text.xs,
    lineHeight: leading.xs,
    fontWeight: 500,
    color: color.mutedForeground,
  },
  separator: {
    marginInline: `calc(-1 * ${space.s1})`,
    height: 1,
    backgroundColor: color.border,
  },
  item: {
    position: "relative",
    display: "flex",
    alignItems: "center",
    gap: space.s2,
    cursor: "default",
    userSelect: "none",
    borderRadius: `max(0px, calc(var(--carbon-command-radius, ${radius.xl}) - 2 * ${space.s1}))`,
    paddingInline: space.s2,
    paddingBlock: space.s1_5,
    fontSize: text.sm,
    lineHeight: leading.sm,
    outlineStyle: "none",
    "--carbon-icon-size": space.s4,
  },
  shortcut: {
    marginInlineStart: "auto",
    fontSize: text.xs,
    lineHeight: leading.xs,
    letterSpacing: tracking.widest,
    color: color.mutedForeground,
  },
  dialogContent: {
    overflow: "hidden",
    padding: 0,
  },
  dialogViewport: { alignItems: "start", paddingBlockStart: "33vh" },
})

const commandStyles = {
  root: styles.root,
  list: styles.list,
  empty: styles.empty,
  group: styles.group,
  heading: styles.heading,
  separator: styles.separator,
  item: styles.item,
  itemHighlighted: states.itemHighlight,
  itemDisabled: states.disabled,
  shortcut: styles.shortcut,
}

type Overrides = { xstyle?: StyleXStyles; className?: string }

type StateOverrides = Overrides & { style?: CSSProperties }

type CommandProps<Item> = Omit<
  AutocompletePrimitive.Root.Props<Item>,
  | "inline"
  | "open"
  | "defaultOpen"
  | "onOpenChange"
  | "onOpenChangeComplete"
  | "openOnInputClick"
  | "modal"
  | "items"
> &
  Overrides & {
    render?: useRender.ComponentProps<"div">["render"]
    items: NonNullable<AutocompletePrimitive.Root.Props<Item>["items"]>
  }

function Command<Item>({
  render,
  xstyle,
  className,
  children,
  items,
  autoHighlight = "always",
  keepHighlight = true,
  ...props
}: CommandProps<Item>) {
  const sx = stylex.props(styles.root, xstyle)

  return useRender({
    defaultTagName: "div",
    render,
    props: {
      "data-slot": "command",
      ...sx,
      className: clsx(sx.className, className),
      children: (
        <AutocompletePrimitive.Root
          inline
          open
          autoHighlight={autoHighlight}
          keepHighlight={keepHighlight}
          {...props}
          items={items as readonly Item[]}
        >
          {children}
        </AutocompletePrimitive.Root>
      ),
    },
  })
}

type CommandDialogProps = Omit<ComponentProps<typeof Dialog>, "children"> &
  Overrides & {
    title?: string
    description?: string
    showCloseButton?: boolean
    children: ReactNode
  }

function CommandDialog({
  title = "Command Palette",
  description = "Search for a command to run...",
  children,
  xstyle,
  className,
  showCloseButton = false,
  ...props
}: CommandDialogProps) {
  return (
    <Dialog {...props}>
      <DialogContent
        xstyle={[styles.dialogContent, xstyle]}
        className={className}
        showCloseButton={showCloseButton}
        viewportProps={{ xstyle: styles.dialogViewport }}
      >
        <DialogTitle xstyle={states.srOnly}>{title}</DialogTitle>
        <DialogDescription xstyle={states.srOnly}>{description}</DialogDescription>
        {children}
      </DialogContent>
    </Dialog>
  )
}

type CommandInputProps = Omit<AutocompletePrimitive.Input.Props, "className"> & Overrides

function CommandInput({ xstyle, className, ...props }: CommandInputProps) {
  return (
    <div data-slot="command-input-wrapper" {...stylex.props(styles.inputWrapper)}>
      <InputGroup xstyle={[styles.inputFrame, xstyle]} className={className}>
        <AutocompletePrimitive.Input render={<Input />} {...props} />
        <InputGroupAddon>
          <Search />
        </InputGroupAddon>
      </InputGroup>
    </div>
  )
}

type CommandListProps = Omit<AutocompletePrimitive.List.Props, "className"> & Overrides

function CommandList({ xstyle, className, ...props }: CommandListProps) {
  const sx = stylex.props(styles.list, xstyle)
  return (
    <AutocompletePrimitive.List
      data-slot="command-list"
      {...sx}
      {...props}
      className={clsx("no-scrollbar", sx.className, className)}
    />
  )
}

type CommandEmptyProps = Omit<AutocompletePrimitive.Empty.Props, "className"> & Overrides

function CommandEmpty({ xstyle, className, ...props }: CommandEmptyProps) {
  const sx = stylex.props(styles.empty, xstyle)
  return (
    <AutocompletePrimitive.Empty
      data-slot="command-empty"
      {...sx}
      {...props}
      className={clsx(sx.className, className)}
    />
  )
}

type CommandGroupProps<Item> = Omit<
  AutocompletePrimitive.Group.Props,
  "className" | "children" | "items"
> &
  Overrides & {
    heading?: ReactNode
    items?: readonly Item[]
    children?: ReactNode | ((item: Item, index: number) => ReactNode)
  }

function CommandGroup<Item>({
  heading,
  items,
  xstyle,
  className,
  children,
  ...props
}: CommandGroupProps<Item>) {
  const sx = stylex.props(styles.group, xstyle)
  return (
    <AutocompletePrimitive.Group
      data-slot="command-group"
      items={items}
      {...sx}
      {...props}
      className={clsx(sx.className, className)}
    >
      {heading != null && (
        <AutocompletePrimitive.GroupLabel
          data-slot="command-group-heading"
          {...stylex.props(styles.heading)}
        >
          {heading}
        </AutocompletePrimitive.GroupLabel>
      )}
      {typeof children === "function" ? (
        <AutocompletePrimitive.Collection>{children}</AutocompletePrimitive.Collection>
      ) : (
        children
      )}
    </AutocompletePrimitive.Group>
  )
}

type CommandSeparatorProps = Omit<AutocompletePrimitive.Separator.Props, "className"> & Overrides

function CommandSeparator({ xstyle, className, ...props }: CommandSeparatorProps) {
  const sx = stylex.props(styles.separator, xstyle)
  return (
    <AutocompletePrimitive.Separator
      data-slot="command-separator"
      {...sx}
      {...props}
      className={clsx(sx.className, className)}
    />
  )
}

type CommandItemProps = Omit<AutocompletePrimitive.Item.Props, "className" | "style"> &
  StateOverrides

function CommandItem({ xstyle, style, className, ...props }: CommandItemProps) {
  return (
    <AutocompletePrimitive.Item
      data-slot="command-item"
      {...props}
      {...stateStyles(
        (state) => [
          styles.item,
          state.highlighted && states.itemHighlight,
          state.disabled && states.disabled,
          xstyle,
        ],
        { className, style, hook: "carbon-icons" },
      )}
    />
  )
}

function CommandShortcut({
  render,
  ref,
  style,
  xstyle,
  className,
  ...props
}: useRender.ComponentProps<"span"> & Overrides) {
  const sx = stylex.props(styles.shortcut, xstyle)
  return useRender({
    defaultTagName: "span",
    render,
    ref,
    props: {
      "data-slot": "command-shortcut",
      ...sx,
      ...props,
      className: clsx(sx.className, className),
      style: { ...sx.style, ...style },
    },
  })
}

export {
  Command,
  CommandDialog,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
  CommandShortcut,
  commandStyles,
}

Update the import paths to match your project setup.

Usage

import {
  Command,
  CommandDialog,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
  CommandShortcut,
} from "@/components/ui/command"
const actions = [
  { value: "calendar", label: "Calendar" },
  { value: "emoji", label: "Search Emoji" },
  { value: "calculator", label: "Calculator" },
]

export function Example() {
  return (
    <Command items={actions}>
      <CommandInput placeholder="Type a command or search..." aria-label="Search commands" />
      <CommandEmpty>No results found.</CommandEmpty>
      <CommandList>
        <CommandGroup heading="Suggestions" items={actions}>
          {(item: (typeof actions)[number]) => (
            <CommandItem key={item.value} value={item} onClick={() => run(item)}>
              {item.label}
            </CommandItem>
          )}
        </CommandGroup>
      </CommandList>
    </Command>
  )
}

Items shaped { value, label } filter on label. For any other shape, pass itemToStringValue to Command.

Composition

Command
├── CommandInput
├── CommandEmpty
└── CommandList
    ├── CommandGroup
    │   └── CommandItem
    ├── CommandSeparator
    └── CommandGroup
        └── CommandItem

CommandEmpty sits before CommandList, not inside it. The list is a listbox, and a status message is not one of its options.

Groups

Pass groups shaped { value, items } to items. CommandList takes a function child that receives each group; render a CommandGroup with the group's items and a function child that receives each item.

"use client"

import { Fragment, useState } from "react"
import { Button } from "@/components/ui/button"
import {
  Command,
  CommandDialog,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
  CommandShortcut,
} from "@/components/ui/command"
import { Calculator, Calendar, CreditCard, Settings, Smile, User } from "lucide-react"
import type { LucideIcon } from "lucide-react"

type Action = { value: string; label: string; icon: LucideIcon; shortcut?: string }
type Group = { value: string; items: Action[] }

const groups: Group[] = [
  {
    value: "Suggestions",
    items: [
      { value: "calendar", label: "Calendar", icon: Calendar },
      { value: "emoji", label: "Search Emoji", icon: Smile },
      { value: "calculator", label: "Calculator", icon: Calculator },
    ],
  },
  {
    value: "Settings",
    items: [
      { value: "profile", label: "Profile", icon: User, shortcut: "⌘P" },
      { value: "billing", label: "Billing", icon: CreditCard, shortcut: "⌘B" },
      { value: "settings", label: "Settings", icon: Settings, shortcut: "⌘S" },
    ],
  },
]

export default function CommandGroups() {
  const [open, setOpen] = useState(false)

  return (
    <>
      <Button variant="outline" onClick={() => setOpen(true)}>
        Open Menu
      </Button>
      <CommandDialog open={open} onOpenChange={setOpen}>
        <Command items={groups}>
          <CommandInput placeholder="Type a command or search..." aria-label="Search commands" />
          <CommandEmpty>No results found.</CommandEmpty>
          <CommandList>
            {(group: Group, index: number) => (
              <Fragment key={group.value}>
                {index > 0 && <CommandSeparator />}
                <CommandGroup heading={group.value} items={group.items}>
                  {(item: Action) => (
                    <CommandItem key={item.value} value={item} onClick={() => setOpen(false)}>
                      <item.icon />
                      <span>{item.label}</span>
                      {item.shortcut && <CommandShortcut>{item.shortcut}</CommandShortcut>}
                    </CommandItem>
                  )}
                </CommandGroup>
              </Fragment>
            )}
          </CommandList>
        </Command>
      </CommandDialog>
    </>
  )
}

Filtering drops a group with no match left, so put the CommandSeparator before every group but the first rather than between groups.

Basic

Put the Command inside a CommandDialog and control it with open and onOpenChange. Close it from an item's onClick.

"use client"

import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
  Command,
  CommandDialog,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command"

type Action = { value: string; label: string }

const suggestions: Action[] = [
  { value: "calendar", label: "Calendar" },
  { value: "emoji", label: "Search Emoji" },
  { value: "calculator", label: "Calculator" },
]

export default function CommandBasic() {
  const [open, setOpen] = useState(false)

  return (
    <>
      <Button variant="outline" onClick={() => setOpen(true)}>
        Open Menu
      </Button>
      <CommandDialog open={open} onOpenChange={setOpen}>
        <Command items={suggestions}>
          <CommandInput placeholder="Type a command or search..." aria-label="Search commands" />
          <CommandEmpty>No results found.</CommandEmpty>
          <CommandList>
            <CommandGroup heading="Suggestions" items={suggestions}>
              {(item: Action) => (
                <CommandItem key={item.value} value={item} onClick={() => setOpen(false)}>
                  {item.label}
                </CommandItem>
              )}
            </CommandGroup>
          </CommandList>
        </Command>
      </CommandDialog>
    </>
  )
}

Dialog

Toggle the dialog from a keyboard shortcut. Nothing in the component binds one.

Press ⌘J

"use client"

import { Fragment, useEffect, useState } from "react"
import * as stylex from "@stylexjs/stylex"
import {
  Command,
  CommandDialog,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
  CommandShortcut,
} from "@/components/ui/command"
import { Kbd, KbdGroup } from "@/components/ui/kbd"
import { color, leading, text } from "@/theme/tokens.stylex"
import { Calculator, Calendar, CreditCard, Settings, Smile, User } from "lucide-react"
import type { LucideIcon } from "lucide-react"

type Action = { value: string; label: string; icon: LucideIcon; shortcut?: string }
type Group = { value: string; items: Action[] }

const groups: Group[] = [
  {
    value: "Suggestions",
    items: [
      { value: "calendar", label: "Calendar", icon: Calendar },
      { value: "emoji", label: "Search Emoji", icon: Smile },
      { value: "calculator", label: "Calculator", icon: Calculator },
    ],
  },
  {
    value: "Settings",
    items: [
      { value: "profile", label: "Profile", icon: User, shortcut: "⌘P" },
      { value: "billing", label: "Billing", icon: CreditCard, shortcut: "⌘B" },
      { value: "settings", label: "Settings", icon: Settings, shortcut: "⌘S" },
    ],
  },
]

const styles = stylex.create({
  hint: { color: color.mutedForeground, fontSize: text.sm, lineHeight: leading.sm },
})

export default function CommandDialogDemo() {
  const [open, setOpen] = useState(false)

  useEffect(() => {
    const down = (event: KeyboardEvent) => {
      if (event.key === "j" && (event.metaKey || event.ctrlKey)) {
        event.preventDefault()
        setOpen((open) => !open)
      }
    }

    document.addEventListener("keydown", down)
    return () => document.removeEventListener("keydown", down)
  }, [])

  return (
    <>
      <p {...stylex.props(styles.hint)}>
        Press{" "}
        <KbdGroup>
          <Kbd>⌘</Kbd>
          <Kbd>J</Kbd>
        </KbdGroup>
      </p>
      <CommandDialog open={open} onOpenChange={setOpen}>
        <Command items={groups}>
          <CommandInput placeholder="Type a command or search..." aria-label="Search commands" />
          <CommandEmpty>No results found.</CommandEmpty>
          <CommandList>
            {(group: Group, index: number) => (
              <Fragment key={group.value}>
                {index > 0 && <CommandSeparator />}
                <CommandGroup heading={group.value} items={group.items}>
                  {(item: Action) => (
                    <CommandItem key={item.value} value={item} onClick={() => setOpen(false)}>
                      <item.icon />
                      <span>{item.label}</span>
                      {item.shortcut && <CommandShortcut>{item.shortcut}</CommandShortcut>}
                    </CommandItem>
                  )}
                </CommandGroup>
              </Fragment>
            )}
          </CommandList>
        </Command>
      </CommandDialog>
    </>
  )
}

Shortcuts

Use CommandShortcut to show a key combination at the trailing edge of an item. It displays the key only.

"use client"

import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
  Command,
  CommandDialog,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandShortcut,
} from "@/components/ui/command"
import { CreditCard, Settings, User } from "lucide-react"
import type { LucideIcon } from "lucide-react"

type Action = { value: string; label: string; icon: LucideIcon; shortcut: string }

const settings: Action[] = [
  { value: "profile", label: "Profile", icon: User, shortcut: "⌘P" },
  { value: "billing", label: "Billing", icon: CreditCard, shortcut: "⌘B" },
  { value: "settings", label: "Settings", icon: Settings, shortcut: "⌘S" },
]

export default function CommandShortcuts() {
  const [open, setOpen] = useState(false)

  return (
    <>
      <Button variant="outline" onClick={() => setOpen(true)}>
        Open Menu
      </Button>
      <CommandDialog open={open} onOpenChange={setOpen}>
        <Command items={settings}>
          <CommandInput placeholder="Type a command or search..." aria-label="Search commands" />
          <CommandEmpty>No results found.</CommandEmpty>
          <CommandList>
            <CommandGroup heading="Settings" items={settings}>
              {(item: Action) => (
                <CommandItem key={item.value} value={item} onClick={() => setOpen(false)}>
                  <item.icon />
                  <span>{item.label}</span>
                  <CommandShortcut>{item.shortcut}</CommandShortcut>
                </CommandItem>
              )}
            </CommandGroup>
          </CommandList>
        </Command>
      </CommandDialog>
    </>
  )
}

Scrollable

The list stops at 18rem and scrolls. The highlighted item scrolls into view.

"use client"

import { Fragment, useState } from "react"
import { Button } from "@/components/ui/button"
import {
  Command,
  CommandDialog,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
  CommandShortcut,
} from "@/components/ui/command"
import {
  Bell,
  Calculator,
  Calendar,
  ClipboardPaste,
  Code,
  Copy,
  CreditCard,
  FileText,
  Folder,
  FolderPlus,
  HelpCircle,
  Home,
  Image,
  Inbox,
  LayoutGrid,
  List,
  Plus,
  Scissors,
  Settings,
  Trash,
  User,
  ZoomIn,
  ZoomOut,
} from "lucide-react"
import type { LucideIcon } from "lucide-react"

type Action = { value: string; label: string; icon: LucideIcon; shortcut?: string }
type Group = { value: string; items: Action[] }

const groups: Group[] = [
  {
    value: "Navigation",
    items: [
      { value: "home", label: "Home", icon: Home, shortcut: "⌘H" },
      { value: "inbox", label: "Inbox", icon: Inbox, shortcut: "⌘I" },
      { value: "documents", label: "Documents", icon: FileText, shortcut: "⌘D" },
      { value: "folders", label: "Folders", icon: Folder, shortcut: "⌘F" },
    ],
  },
  {
    value: "Actions",
    items: [
      { value: "new-file", label: "New File", icon: Plus, shortcut: "⌘N" },
      { value: "new-folder", label: "New Folder", icon: FolderPlus, shortcut: "⇧⌘N" },
      { value: "copy", label: "Copy", icon: Copy, shortcut: "⌘C" },
      { value: "cut", label: "Cut", icon: Scissors, shortcut: "⌘X" },
      { value: "paste", label: "Paste", icon: ClipboardPaste, shortcut: "⌘V" },
      { value: "delete", label: "Delete", icon: Trash, shortcut: "⌫" },
    ],
  },
  {
    value: "View",
    items: [
      { value: "grid", label: "Grid View", icon: LayoutGrid },
      { value: "list", label: "List View", icon: List },
      { value: "zoom-in", label: "Zoom In", icon: ZoomIn, shortcut: "⌘+" },
      { value: "zoom-out", label: "Zoom Out", icon: ZoomOut, shortcut: "⌘-" },
    ],
  },
  {
    value: "Account",
    items: [
      { value: "profile", label: "Profile", icon: User, shortcut: "⌘P" },
      { value: "billing", label: "Billing", icon: CreditCard, shortcut: "⌘B" },
      { value: "settings", label: "Settings", icon: Settings, shortcut: "⌘S" },
      { value: "notifications", label: "Notifications", icon: Bell },
      { value: "help", label: "Help & Support", icon: HelpCircle },
    ],
  },
  {
    value: "Tools",
    items: [
      { value: "calculator", label: "Calculator", icon: Calculator },
      { value: "calendar", label: "Calendar", icon: Calendar },
      { value: "image-editor", label: "Image Editor", icon: Image },
      { value: "code-editor", label: "Code Editor", icon: Code },
    ],
  },
]

export default function CommandScrollable() {
  const [open, setOpen] = useState(false)

  return (
    <>
      <Button variant="outline" onClick={() => setOpen(true)}>
        Open Menu
      </Button>
      <CommandDialog open={open} onOpenChange={setOpen}>
        <Command items={groups}>
          <CommandInput placeholder="Type a command or search..." aria-label="Search commands" />
          <CommandEmpty>No results found.</CommandEmpty>
          <CommandList>
            {(group: Group, index: number) => (
              <Fragment key={group.value}>
                {index > 0 && <CommandSeparator />}
                <CommandGroup heading={group.value} items={group.items}>
                  {(item: Action) => (
                    <CommandItem key={item.value} value={item} onClick={() => setOpen(false)}>
                      <item.icon />
                      <span>{item.label}</span>
                      {item.shortcut && <CommandShortcut>{item.shortcut}</CommandShortcut>}
                    </CommandItem>
                  )}
                </CommandGroup>
              </Fragment>
            )}
          </CommandList>
        </Command>
      </CommandDialog>
    </>
  )
}

API reference

Pass render to replace a part's element or compose it with another component.

Every part wraps the Base UI Autocomplete part of the same name and accepts its props. See the Base UI reference for the full list. Each styled part also takes xstyle and className. CommandItem also takes style, which is real inline CSS merged over the style its Base UI state resolves to.

Command

PropTypeDefault
itemsreadonly Item[] | readonly Group[]—
autoHighlightboolean | "always""always"
keepHighlightbooleantrue
itemToStringValue(item: Item) => string—
filter(item, query, toString) => boolean—
valuestring—
onValueChange(value: string, details) => void—

value is the input's text. inline and open are set and cannot be changed.

CommandDialog

PropTypeDefault
openboolean—
onOpenChange(open: boolean, details) => void—
titlestring"Command Palette"
descriptionstring"Search for a command to run..."
showCloseButtonbooleanfalse

title and description are read to screen readers and not shown. The popup sits a third of the way down the viewport and stays put as the list changes height.

CommandGroup

PropTypeDefault
headingReactNode—
itemsreadonly Item[]—
childrenReactNode | (item: Item, index: number) => ReactNode—

CommandItem

PropTypeDefault
valueItem—
onClick(event) => void—
disabledbooleanfalse

commandStyles

The style maps behind the component. Spread them onto any element.

KeyWhat it is
rootThe panel: surface, radius, padding
listThe scrolling list
emptyThe empty message
groupA group's padding
headingA group heading
separatorThe hairline between groups
itemAn item's row and type
itemHighlightedThe highlight fill and inset ring
itemDisabledThe dimmed, inert item
shortcutCommandShortcut

itemDisabled is disabled from states in theme/states.ts, the same dim every other control uses. root keeps its own fill. The panel is inline, so it takes no popup ring or shadow.

Data attributes

AttributeOnEffect
data-slot="command"the panelTargets the panel from a parent
data-slot="input-group"the fieldThe frame CommandInput renders
data-slot="command-list"the listTargets the list from a parent
data-slot="command-group"a groupTargets a group from a parent
data-slot="command-group-heading"a headingTargets a heading from a parent
data-slot="command-item"an itemTargets items from a parent
data-highlighted, data-disabledan itemBase UI writes them
data-slot="dialog-content"the dialogTargets the animated dialog panel