# Command Palette

A searchable command menu that opens in a dialog on desktop and a drawer on mobile.

Command Palette is an open-source pattern from suiss/ui for React. The @esuiss/ui CLI copies its source into your project, so you own the code.

Source: https://ui.suiss.app/docs/patterns/command-palette

**React**

**Example**

```tsx
import {
  CalculatorIcon,
  CalendarBlankIcon,
  CreditCardIcon,
  GearIcon,
  SmileyIcon,
  UserIcon,
} from "@phosphor-icons/react"
import {
  CommandPalette,
  CommandPaletteContent,
  CommandPaletteTrigger,
} from "@/components/patterns/command-palette"
import { Button } from "@/components/ui/button"
import {
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
  CommandShortcut,
} from "@/components/ui/command"
import { Kbd, KbdGroup } from "@/components/ui/kbd"

function PaletteItems() {
  return (
    <>
      <CommandInput placeholder="Type a command or search..." />
      <CommandList>
        <CommandEmpty>No results found.</CommandEmpty>
        <CommandGroup heading="Suggestions">
          <CommandItem>
            <CalendarBlankIcon />
            Calendar
          </CommandItem>
          <CommandItem>
            <SmileyIcon />
            Search emoji
          </CommandItem>
          <CommandItem disabled>
            <CalculatorIcon />
            Calculator
          </CommandItem>
        </CommandGroup>
        <CommandSeparator />
        <CommandGroup heading="Settings">
          <CommandItem>
            <UserIcon />
            Profile
            <CommandShortcut mod>P</CommandShortcut>
          </CommandItem>
          <CommandItem>
            <CreditCardIcon />
            Billing
            <CommandShortcut mod>B</CommandShortcut>
          </CommandItem>
          <CommandItem>
            <GearIcon />
            Settings
            <CommandShortcut mod>S</CommandShortcut>
          </CommandItem>
        </CommandGroup>
      </CommandList>
    </>
  )
}

function Trigger() {
  return (
    <CommandPaletteTrigger render={<Button variant="outline" />}>
      Search
      <KbdGroup>
        <Kbd mod />
        <Kbd>K</Kbd>
      </KbdGroup>
    </CommandPaletteTrigger>
  )
}

export default function CommandPaletteDemo() {
  return (
    <div className="flex min-h-svh items-center justify-center">
      <CommandPalette>
        <Trigger />
        <CommandPaletteContent>
          <PaletteItems />
        </CommandPaletteContent>
      </CommandPalette>
    </div>
  )
}
```

`CommandPalette` hosts a `Command` list inside a `Dialog` on desktop, positioned in the upper third of the viewport, and inside a bottom `Drawer` with a swipe handle below the `48rem` breakpoint. Fill `CommandPaletteContent` with `CommandInput`, `CommandList`, `CommandGroup`, `CommandItem` and the other `Command` parts.

A global keyboard shortcut toggles the palette: `Cmd+K` on macOS and `Ctrl+K` elsewhere by default. Use it for app-wide search and quick actions.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/command-palette
```

Works in any project with a `components.json`. See [Installation](https://ui.suiss.app/docs/installation#add-components) for the one-line registry setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add patterns command-palette
```

### Manual

1. Add the [`cn` helper](https://ui.suiss.app/docs/installation#the-cn-helper).

2. Add the pieces it builds on: [Command](https://ui.suiss.app/docs/components/command), [Dialog](https://ui.suiss.app/docs/components/dialog), [Drawer](https://ui.suiss.app/docs/components/drawer), `use-mobile`, `use-platform`.

3. Copy the source into `components/patterns/command-palette.tsx`:

```tsx title="components/patterns/command-palette.tsx"
"use client"

import * as React from "react"
import { Command } from "@/components/ui/command"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import {
  Drawer,
  DrawerContent,
  DrawerDescription,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
import { useIsMobile } from "@/hooks/use-mobile"
import { isMacPlatform } from "@/hooks/use-platform"
import { cn } from "@/lib/utils"

type CommandPaletteContextProps = {
  isMobile: boolean
}

const CommandPaletteContext =
  React.createContext<CommandPaletteContextProps | null>(null)

function useCommandPalette() {
  const context = React.useContext(CommandPaletteContext)

  if (!context) {
    throw new Error(
      "useCommandPalette must be used within a <CommandPalette />"
    )
  }

  return context
}

function CommandPalette({
  open: openProp,
  defaultOpen = false,
  onOpenChange,
  hotkey = "k",
  floating,
  children,
}: Pick<
  React.ComponentProps<typeof Dialog>,
  "open" | "defaultOpen" | "children"
> & {
  onOpenChange?: (open: boolean) => void
  hotkey?: string | false
  floating?: boolean
}) {
  const isMobile = useIsMobile()
  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
  const open = openProp ?? uncontrolledOpen
  const openRef = React.useRef(open)
  openRef.current = open

  const setOpen = React.useCallback(
    (next: boolean) => {
      if (openProp === undefined) setUncontrolledOpen(next)
      onOpenChange?.(next)
    },
    [openProp, onOpenChange]
  )

  React.useEffect(() => {
    if (!hotkey) return
    const handleKeyDown = (event: KeyboardEvent) => {
      if (
        event.key.toLowerCase() === hotkey.toLowerCase() &&
        (isMacPlatform() ? event.metaKey : event.ctrlKey)
      ) {
        event.preventDefault()
        setOpen(!openRef.current)
      }
    }
    document.addEventListener("keydown", handleKeyDown)
    return () => document.removeEventListener("keydown", handleKeyDown)
  }, [hotkey, setOpen])

  const rootProps = {
    open,
    onOpenChange: (next: boolean) => setOpen(next),
    children,
  }

  return (
    <CommandPaletteContext.Provider value={{ isMobile }}>
      {isMobile ? (
        <Drawer
          data-slot="command-palette"
          floating={floating}
          showSwipeHandle
          {...rootProps}
        />
      ) : (
        <Dialog data-slot="command-palette" {...rootProps} />
      )}
    </CommandPaletteContext.Provider>
  )
}

function CommandPaletteTrigger(
  props: React.ButtonHTMLAttributes<HTMLButtonElement> & {
    render?: React.ReactElement
  }
) {
  const { isMobile } = useCommandPalette()
  const Trigger = isMobile ? DrawerTrigger : DialogTrigger

  return <Trigger data-slot="command-palette-trigger" {...props} />
}

function CommandPaletteContent({
  title = "Command Palette",
  description = "Search for a command to run...",
  showCloseButton = false,
  closeLabel,
  className,
  children,
  ...props
}: React.ComponentProps<typeof Command> & {
  title?: string
  description?: string
  showCloseButton?: boolean
  closeLabel?: string
}) {
  const { isMobile } = useCommandPalette()

  if (isMobile) {
    return (
      <DrawerContent
        data-slot="command-palette-content"
        showCloseButton={showCloseButton}
        closeLabel={closeLabel}
      >
        <DrawerHeader className="sr-only">
          <DrawerTitle>{title}</DrawerTitle>
          <DrawerDescription>{description}</DrawerDescription>
        </DrawerHeader>
        <Command
          className={cn(
            "rounded-none bg-transparent px-3 pt-1.25 pb-[max(--spacing(3),env(safe-area-inset-bottom))] **:data-[slot=command-list]:max-h-[60dvh]",
            showCloseButton && "**:data-[slot=command-input-wrapper]:pe-11",
            showCloseButton &&
              closeLabel &&
              "**:data-[slot=command-input-wrapper]:pe-21",
            className
          )}
          {...props}
        >
          {children}
        </Command>
      </DrawerContent>
    )
  }

  return (
    <DialogContent
      data-slot="command-palette-content"
      showCloseButton={showCloseButton}
      closeLabel={closeLabel}
      className="top-1/3 translate-y-0 overflow-hidden rounded-2xl! p-0"
    >
      <DialogHeader className="sr-only">
        <DialogTitle>{title}</DialogTitle>
        <DialogDescription>{description}</DialogDescription>
      </DialogHeader>
      <Command className={className} {...props}>
        {children}
      </Command>
    </DialogContent>
  )
}

export { CommandPalette, CommandPaletteContent, CommandPaletteTrigger }
```

4. Update the import paths to match your project.

## Usage

```tsx
import { CommandPalette, CommandPaletteTrigger, CommandPaletteContent } from "@/components/patterns/command-palette"
```

```tsx
<div className="flex min-h-svh items-center justify-center">
  <CommandPalette>
    <Trigger />
    <CommandPaletteContent>
      <PaletteItems />
    </CommandPaletteContent>
  </CommandPalette>
</div>
```

The palette can be controlled with `open` and `onOpenChange` or uncontrolled with `defaultOpen`; the hotkey and the trigger update the same state. Set `hotkey` to another letter to change the shortcut, or to `false` to disable it. `CommandPaletteTrigger` accepts `render` to use your own button, for example a `Button` with a `Kbd` hint. Remaining props on `CommandPaletteContent` pass through to `Command`.

## Examples

### With close button

Shows an icon close button with `showCloseButton`.

**Example**

```tsx
import {
  CalculatorIcon,
  CalendarBlankIcon,
  CreditCardIcon,
  GearIcon,
  SmileyIcon,
  UserIcon,
} from "@phosphor-icons/react"
import {
  CommandPalette,
  CommandPaletteContent,
  CommandPaletteTrigger,
} from "@/components/patterns/command-palette"
import { Button } from "@/components/ui/button"
import {
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
  CommandShortcut,
} from "@/components/ui/command"
import { Kbd, KbdGroup } from "@/components/ui/kbd"

function PaletteItems() {
  return (
    <>
      <CommandInput placeholder="Type a command or search..." />
      <CommandList>
        <CommandEmpty>No results found.</CommandEmpty>
        <CommandGroup heading="Suggestions">
          <CommandItem>
            <CalendarBlankIcon />
            Calendar
          </CommandItem>
          <CommandItem>
            <SmileyIcon />
            Search emoji
          </CommandItem>
          <CommandItem disabled>
            <CalculatorIcon />
            Calculator
          </CommandItem>
        </CommandGroup>
        <CommandSeparator />
        <CommandGroup heading="Settings">
          <CommandItem>
            <UserIcon />
            Profile
            <CommandShortcut mod>P</CommandShortcut>
          </CommandItem>
          <CommandItem>
            <CreditCardIcon />
            Billing
            <CommandShortcut mod>B</CommandShortcut>
          </CommandItem>
          <CommandItem>
            <GearIcon />
            Settings
            <CommandShortcut mod>S</CommandShortcut>
          </CommandItem>
        </CommandGroup>
      </CommandList>
    </>
  )
}

function Trigger() {
  return (
    <CommandPaletteTrigger render={<Button variant="outline" />}>
      Search
      <KbdGroup>
        <Kbd mod />
        <Kbd>K</Kbd>
      </KbdGroup>
    </CommandPaletteTrigger>
  )
}

export default function CommandPaletteWithCloseButton() {
  return (
    <div className="flex min-h-svh items-center justify-center">
      <CommandPalette>
        <Trigger />
        <CommandPaletteContent showCloseButton>
          <PaletteItems />
        </CommandPaletteContent>
      </CommandPalette>
    </div>
  )
}
```

### With close label

Shows a text close button labelled Cancel with `closeLabel`.

**Example**

```tsx
import {
  CalculatorIcon,
  CalendarBlankIcon,
  CreditCardIcon,
  GearIcon,
  SmileyIcon,
  UserIcon,
} from "@phosphor-icons/react"
import {
  CommandPalette,
  CommandPaletteContent,
  CommandPaletteTrigger,
} from "@/components/patterns/command-palette"
import { Button } from "@/components/ui/button"
import {
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
  CommandShortcut,
} from "@/components/ui/command"
import { Kbd, KbdGroup } from "@/components/ui/kbd"

function PaletteItems() {
  return (
    <>
      <CommandInput placeholder="Type a command or search..." />
      <CommandList>
        <CommandEmpty>No results found.</CommandEmpty>
        <CommandGroup heading="Suggestions">
          <CommandItem>
            <CalendarBlankIcon />
            Calendar
          </CommandItem>
          <CommandItem>
            <SmileyIcon />
            Search emoji
          </CommandItem>
          <CommandItem disabled>
            <CalculatorIcon />
            Calculator
          </CommandItem>
        </CommandGroup>
        <CommandSeparator />
        <CommandGroup heading="Settings">
          <CommandItem>
            <UserIcon />
            Profile
            <CommandShortcut mod>P</CommandShortcut>
          </CommandItem>
          <CommandItem>
            <CreditCardIcon />
            Billing
            <CommandShortcut mod>B</CommandShortcut>
          </CommandItem>
          <CommandItem>
            <GearIcon />
            Settings
            <CommandShortcut mod>S</CommandShortcut>
          </CommandItem>
        </CommandGroup>
      </CommandList>
    </>
  )
}

function Trigger() {
  return (
    <CommandPaletteTrigger render={<Button variant="outline" />}>
      Search
      <KbdGroup>
        <Kbd mod />
        <Kbd>K</Kbd>
      </KbdGroup>
    </CommandPaletteTrigger>
  )
}

export default function CommandPaletteWithCloseLabel() {
  return (
    <div className="flex min-h-svh items-center justify-center">
      <CommandPalette>
        <Trigger />
        <CommandPaletteContent showCloseButton closeLabel="Cancel">
          <PaletteItems />
        </CommandPaletteContent>
      </CommandPalette>
    </div>
  )
}
```

### Floating

Insets the mobile drawer from the screen edges with `floating`.

**Example**

```tsx
import {
  CalculatorIcon,
  CalendarBlankIcon,
  CreditCardIcon,
  GearIcon,
  SmileyIcon,
  UserIcon,
} from "@phosphor-icons/react"
import {
  CommandPalette,
  CommandPaletteContent,
  CommandPaletteTrigger,
} from "@/components/patterns/command-palette"
import { Button } from "@/components/ui/button"
import {
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
  CommandShortcut,
} from "@/components/ui/command"
import { Kbd, KbdGroup } from "@/components/ui/kbd"

function PaletteItems() {
  return (
    <>
      <CommandInput placeholder="Type a command or search..." />
      <CommandList>
        <CommandEmpty>No results found.</CommandEmpty>
        <CommandGroup heading="Suggestions">
          <CommandItem>
            <CalendarBlankIcon />
            Calendar
          </CommandItem>
          <CommandItem>
            <SmileyIcon />
            Search emoji
          </CommandItem>
          <CommandItem disabled>
            <CalculatorIcon />
            Calculator
          </CommandItem>
        </CommandGroup>
        <CommandSeparator />
        <CommandGroup heading="Settings">
          <CommandItem>
            <UserIcon />
            Profile
            <CommandShortcut mod>P</CommandShortcut>
          </CommandItem>
          <CommandItem>
            <CreditCardIcon />
            Billing
            <CommandShortcut mod>B</CommandShortcut>
          </CommandItem>
          <CommandItem>
            <GearIcon />
            Settings
            <CommandShortcut mod>S</CommandShortcut>
          </CommandItem>
        </CommandGroup>
      </CommandList>
    </>
  )
}

function Trigger() {
  return (
    <CommandPaletteTrigger render={<Button variant="outline" />}>
      Search
      <KbdGroup>
        <Kbd mod />
        <Kbd>K</Kbd>
      </KbdGroup>
    </CommandPaletteTrigger>
  )
}

export default function CommandPaletteFloating() {
  return (
    <div className="flex min-h-svh items-center justify-center">
      <CommandPalette floating>
        <Trigger />
        <CommandPaletteContent>
          <PaletteItems />
        </CommandPaletteContent>
      </CommandPalette>
    </div>
  )
}
```

## API Reference

- `CommandPalette`
- `CommandPaletteContent`

### CommandPalette

The root that renders a `Dialog` on desktop and a `Drawer` on mobile and registers the hotkey.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `onOpenChange` | `(open: boolean) => void` | - | Called when the palette opens or closes, including through the hotkey. |
| `hotkey` | `string \| false` | `"k"` | The letter that toggles the palette together with `Cmd` on macOS or `Ctrl` elsewhere; `false` disables it. |
| `floating` | `boolean` | - | Insets the mobile drawer from the screen edges with fully rounded corners. |

Also accepts the props of `React.ComponentProps<typeof Dialog>`.

### CommandPaletteTrigger

The button that opens the palette.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `React.ReactElement` | - | Replaces the default button element with your own element, such as a `Button`. |

Also accepts the props of `React.ButtonHTMLAttributes<HTMLButtonElement>`.

### CommandPaletteContent

The popup that wraps a `Command` with a visually hidden title and description.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` | `string` | `"Command Palette"` | Accessible name of the dialog, rendered visually hidden. |
| `description` | `string` | `"Search for a command to run..."` | Accessible description of the dialog, rendered visually hidden. |
| `showCloseButton` | `boolean` | `false` | Shows a close button in the corner of the popup. |
| `closeLabel` | `string` | - | Replaces the close icon with this text; requires `showCloseButton`. |

Also accepts the props of `React.ComponentProps<typeof Command>`.

## Accessibility

- The popup is a `dialog` named by `title` and described by `description`.
- Opening the palette focuses the search input, and the first enabled option is selected.
- Arrow keys move between options and skip disabled ones; typing filters the list.
- `Escape` closes the palette and returns focus to the trigger.

## Notes

Parts carry `data-slot` values `command-palette`, `command-palette-trigger` and `command-palette-content`. On mobile the list is capped at `60dvh` and the input gains end padding when a close button is shown. `className` on `CommandPaletteContent` applies to the inner `Command`, not the dialog or drawer.

