# Command

A search field above a filtered list of commands or pages, inline or in a dialog.

- **Import:** `import { Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut, CommandSeparator } from "@booleanpress/ui/command"`
- **Also install:** `cmdk`
- **APG Combobox:** <https://www.w3.org/WAI/ARIA/apg/patterns/combobox/>
- **Page:** <https://ui.booleanpress.com/components/command> · @booleanpress/ui 0.1.0

## Usage

Command is [cmdk](https://github.com/dip/cmdk) with the package's look. Typing filters the items; the first match is highlighted and Enter runs its `onSelect`. Name the search field with cmdk's `label` prop on `Command`.

```tsx
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@booleanpress/ui/command"

export function Pages() {
  return (
    <Command label="Search pages">
      <CommandInput placeholder="Search for a page" />
      <CommandList>
        <CommandEmpty>No results.</CommandEmpty>
        <CommandGroup heading="Pages">
          <CommandItem onSelect={() => open("/logs")}>Email log</CommandItem>
          <CommandItem onSelect={() => open("/mailers")}>Mailers</CommandItem>
        </CommandGroup>
      </CommandList>
    </Command>
  )
}
```

`CommandDialog` puts the same parts in a [dialog](/components/dialog); control it with `open` and `onOpenChange`, and open it from a button and a shortcut of your own. Its hidden title and description are inside the dialog and come from the provider's `commandTitle` and `commandDescription`; pass `title` and `description` to override them. The dialog's title also names the search field. Give `CommandItem` a `value` when its text is not what people should search, `keywords` for extra search words, and `disabled` to skip it. `CommandShortcut` shows a key hint; it does not bind the key. cmdk filters by itself: pass `shouldFilter={false}` to `Command` and filter the items yourself for server-side search.

## Examples

### Basic

An inline list with two groups and shortcut hints.

```tsx
import { FileTextIcon, MailIcon, PlugIcon, SettingsIcon, UserIcon } from "lucide-react"
import {
  Command,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandShortcut,
  CommandEmpty,
} from "@booleanpress/ui/command"

export default function CommandBasic() {
  return (
    <Command label="Search pages and actions" className="max-w-sm rounded-lg border shadow-md">
      <CommandInput placeholder="Search for a page or action" />
      <CommandList>
        <CommandEmpty>No results.</CommandEmpty>
        <CommandGroup heading="Pages">
          <CommandItem>
            <MailIcon />
            Email log
            <CommandShortcut>⌘L</CommandShortcut>
          </CommandItem>
          <CommandItem>
            <PlugIcon />
            Mailers
            <CommandShortcut>⌘M</CommandShortcut>
          </CommandItem>
          <CommandItem>
            <FileTextIcon />
            Routing rules
          </CommandItem>
        </CommandGroup>
        <CommandGroup heading="Account">
          <CommandItem>
            <UserIcon />
            Profile
          </CommandItem>
          <CommandItem>
            <SettingsIcon />
            Settings
            <CommandShortcut>⌘,</CommandShortcut>
          </CommandItem>
        </CommandGroup>
      </CommandList>
    </Command>
  )
}
```

### In a dialog

`CommandDialog`, opened by the button or by ⌘J / Ctrl+J.

```tsx
import { useEffect, useState } from "react"
import { MailIcon, PlugIcon, SettingsIcon } from "lucide-react"
import { Button } from "@booleanpress/ui/button"
import {
  CommandDialog,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandShortcut,
} from "@booleanpress/ui/command"

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

  useEffect(() => {
    const onKeyDown = (event: KeyboardEvent) => {
      if (event.key === "j" && (event.metaKey || event.ctrlKey)) {
        event.preventDefault()
        setOpen(true)
      }
    }
    document.addEventListener("keydown", onKeyDown)
    return () => document.removeEventListener("keydown", onKeyDown)
  }, [])

  return (
    <>
      <Button variant="outline" onClick={() => setOpen(true)}>
        Search
        <kbd className="ms-2 rounded border px-1.5 font-mono text-xs text-muted-foreground">⌘J</kbd>
      </Button>
      <CommandDialog open={open} onOpenChange={setOpen}>
        <CommandInput placeholder="Type a command or search" />
        <CommandList>
          <CommandEmpty>No results.</CommandEmpty>
          <CommandGroup heading="Go to">
            <CommandItem onSelect={() => setOpen(false)}>
              <MailIcon />
              Email log
            </CommandItem>
            <CommandItem onSelect={() => setOpen(false)}>
              <PlugIcon />
              Mailers
            </CommandItem>
            <CommandItem onSelect={() => setOpen(false)}>
              <SettingsIcon />
              Settings
              <CommandShortcut>⌘,</CommandShortcut>
            </CommandItem>
          </CommandGroup>
        </CommandList>
      </CommandDialog>
    </>
  )
}
```

### Empty result

`CommandEmpty` shows when nothing matches the search.

```tsx
import { Command, CommandEmpty, CommandInput, CommandItem, CommandList } from "@booleanpress/ui/command"

export default function CommandEmptyExample() {
  return (
    <Command label="Search mailers" className="max-w-sm rounded-lg border shadow-md">
      <CommandInput placeholder="Search mailers" defaultValue="postmark" />
      <CommandList>
        <CommandEmpty>No mailer matches that name.</CommandEmpty>
        <CommandItem>Amazon SES</CommandItem>
        <CommandItem>Mailgun</CommandItem>
        <CommandItem>SendGrid</CommandItem>
      </CommandList>
    </Command>
  )
}
```

### Disabled item

A disabled item is skipped by the arrow keys and cannot be chosen.

```tsx
import { Command, CommandGroup, CommandInput, CommandItem, CommandList } from "@booleanpress/ui/command"

export default function CommandDisabledItem() {
  return (
    <Command label="Search actions" className="max-w-sm rounded-lg border shadow-md">
      <CommandInput placeholder="Search actions" />
      <CommandList>
        <CommandGroup heading="Actions">
          <CommandItem>Send a test email</CommandItem>
          <CommandItem disabled>Export the log (needs Pro)</CommandItem>
          <CommandItem>Clear the cache</CommandItem>
        </CommandGroup>
      </CommandList>
    </Command>
  )
}
```

## Accessibility

**Semantics.** The search field is an `input` with `role="combobox"`, `aria-expanded`, `aria-controls` and `aria-activedescendant`; the list is a `role="listbox"` named from the provider's `suggestions` string, and each item a `role="option"` with `aria-selected`. Groups are `role="group"` named by their heading. Focus stays in the field while the arrow keys move the highlight.

**Labels.** Name an inline command with the `label` prop on `Command`; it names the search field. `CommandDialog` names it with the dialog's title. Translate the list's name, the dialog title and the dialog description in the provider.

**Focus.** Focus goes to the search field when a `CommandDialog` opens and returns to what opened it on close. Inline, the field is a normal tab stop; the list is never focused.

**Known limits.**

- cmdk also binds Ctrl+N, Ctrl+J (next) and Ctrl+P, Ctrl+K (previous) while the field has focus. Pass `vimBindings={false}` to `Command` if your shortcuts collide.
- While nothing matches, cmdk leaves an empty `listbox` with the `CommandEmpty` message beside it; axe reports an empty listbox as `aria-required-children`. The message is still read as text.
- Do not put `CommandSeparator` inside the list: cmdk renders it as `role="separator"` in a `listbox`, which axe reports as `aria-required-children`. Separate groups by their headings.
- `CommandShortcut` is only a hint. Bind the key yourself, and not on a key that cmdk uses.

### Keyboard

| Key | Behaviour |
| --- | --- |
| ↓ | Highlights the next item, wrapping from the last to the first (`loop`). |
| ↑ | Highlights the previous item. |
| Home | Highlights the first item. |
| End | Highlights the last item. |
| Enter | Runs the highlighted item's `onSelect`. |
| Escape | In a `CommandDialog`, closes it and returns focus. |

## API

### Command

Renders cmdk Command and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `disablePointerSelection` | `boolean` |  | Optionally set to `true` to disable selection via pointer events. |
| `filter` | `CommandFilter` |  | Custom filter function for whether each command menu item should matches the given search query. It should return a number between 0 and 1, with 1 being the best match and 0 being hidden entirely. By default, uses the `command-score` library. |
| `label` | `string` |  | Accessible label for this command menu. Not shown visibly. |
| `loop` | `boolean` |  | Optionally set to `true` to turn on looping around when using the arrow keys. |
| `onValueChange` | `((value: string) => void)` |  | Event handler called when the selected item of the menu changes. |
| `shouldFilter` | `boolean` |  | Optionally set to `false` to turn off the automatic filtering and sorting. If `false`, you must conditionally render valid items based on the search query yourself. |
| `value` | `string` |  | Optional controlled state of the selected command menu item. |
| `vimBindings` | `boolean` |  | Set to `false` to disable ctrl+n/j/p/k shortcuts. Defaults to `true`. |

### CommandDialog

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` |  |  |
| `defaultOpen` | `boolean` |  |  |
| `description` | `string` |  | The hidden dialog description. Defaults to the provider's `commandDescription`. |
| `modal` | `boolean` |  |  |
| `onOpenChange` | `((open: boolean) => void)` |  | Called with `true` or `false` when it opens or closes. |
| `open` | `boolean` |  | Whether it is open. Pair it with `onOpenChange`. |
| `showCloseButton` | `boolean` | `true` | Render the × button. `true` by default. |
| `title` | `string` |  | The hidden dialog title, which also names the search field. Defaults to the provider's `commandTitle`. |

### CommandInput

Renders cmdk Command.Input and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `onValueChange` | `((search: string) => void)` |  | Event handler called when the search value changes. |
| `value` | `string` |  | Optional controlled state for the value of the search input. |

### CommandList

Renders cmdk Command.List and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `label` | `string` |  | Accessible label for this List of suggestions. Not shown visibly. |

### CommandEmpty

Renders cmdk Command.Empty and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |

### CommandGroup

Renders cmdk Command.Group and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `forceMount` | `boolean` |  | Whether this group is forcibly rendered regardless of filtering. |
| `heading` | `ReactNode` |  | Optional heading to render for this group. |
| `value` | `string` |  | If no heading is provided, you must provide a value that is unique for this group. |

### CommandItem

Renders cmdk Command.Item and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `disabled` | `boolean` |  | Whether this item is currently disabled. |
| `forceMount` | `boolean` |  | Whether this item is forcibly rendered regardless of filtering. |
| `keywords` | `string[]` |  | Optional keywords to match against when filtering. |
| `onSelect` | `((value: string) => void)` |  | Event handler for when this item is selected, either via click or keyboard selection. |
| `value` | `string` |  | A unique value for this item. If no value is provided, it will be inferred from `children` or the rendered `textContent`. If your `textContent` changes between renders, you _must_ provide a stable, unique `value`. |

### CommandShortcut

Renders a `span` and passes it every other prop.

### CommandSeparator

Renders cmdk Command.Separator and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `alwaysRender` | `boolean` |  | Whether this separator should always be rendered. Useful if you disable automatic filtering. |
| `asChild` | `boolean` |  |  |

Every part takes `className`, merged with its defaults by `cn()`, and `ref`, which reaches the element it renders.

**Data attributes:** `data-slot="command"` (Command), `data-slot="command-input-wrapper"` (CommandInput), `data-slot="command-list"` (CommandList), `data-slot="command-empty"` (CommandEmpty), `data-slot="command-group"` (CommandGroup), `data-slot="command-item"` (CommandItem), `data-slot="command-shortcut"` (CommandShortcut), `data-slot="command-separator"` (CommandSeparator).

**Provider strings:** `commandTitle`, `commandDescription`, `suggestions` (`BooleanUIProvider`'s `strings`).

## Theming

The list is a floating surface: `--popover` and `--popover-foreground`. The highlighted item uses `--accent` and `--accent-foreground`.

| Token | Used for |
| --- | --- |
| `--accent` | background |
| `--accent-foreground` | text |
| `--border` | background |
| `--foreground` | text |
| `--muted-foreground` | text |
| `--popover` | background |
| `--popover-foreground` | text |
