---
title: CommandPalette
description: Search grouped commands and navigation targets from an inline surface.
package: moraine
version: 0.6.0
repository: https://github.com/subframe7536/moraine
---

# CommandPalette

> Search grouped commands and navigation targets from an inline surface.

Use CommandPalette for searchable commands or navigation targets. The application decides when to open it and registers any global shortcut.

## Basic usage

```tsx
import { CommandPalette } from 'moraine'

export function Example() {
  return (
    <CommandPalette
      groups={[{ id: 'pages', label: 'Pages', items: [{ value: 'settings', label: 'Settings' }] }]}
    />
  )
}
```

## Anatomy

```text
CommandPalette [component; slot=root]
├── inputWrapper [slot]
│   ├── inputLeading [slot]
│   ├── input [slot]
│   └── close [slot]
├── listbox [slot]
│   ├── group [slot]
│   │   ├── groupLabel [slot]
│   │   └── item [slot]
│   │       ├── itemLeading [slot]
│   │       ├── itemWrapper [slot]
│   │       │   ├── itemLabel [slot]
│   │       │   └── itemDescription [slot]
│   │       └── itemTrailing [slot]
│   └── empty [slot]
└── footer [slot]
```

Group labels and items may be flattened into sibling rows when virtualized. Trailing descriptions render inside itemLabel.

## Usage

### Groups, filtering, and selection

Pass groups and items from application state. The palette filters the supplied collection and invokes item selection callbacks; fetching commands, routing, and global keyboard shortcuts are application-owned.

```tsx
import { CommandPalette, Icon } from 'moraine'
import type { CommandPaletteT } from 'moraine'
import { createSignal } from 'solid-js'

const COMMAND_GROUPS: CommandPaletteT.Group[] = [
  {
    id: 'workspace',
    label: 'Workspace sections',
    items: [
      {
        value: 'overview',
        label: 'Overview',
        description: 'Recent activity and open work',
        leadingRender: () => <Icon name="i-lucide:layout-dashboard" />,
      },
      {
        value: 'members',
        label: 'Members',
        description: 'People with workspace access',
        leadingRender: () => <Icon name="i-lucide:users" />,
      },
      {
        value: 'security',
        label: 'Security',
        description: 'Sign-in and access policies',
        leadingRender: () => <Icon name="i-lucide:shield-check" />,
      },
    ],
  },
]

export function GroupsFiltering() {
  const [section, setSection] = createSignal('overview')

  return (
    <div class="max-w-md w-full space-y-3">
      <CommandPalette
        autofocus={false}
        placeholder="Find a workspace section..."
        groups={COMMAND_GROUPS}
        onSelect={(item) => setSection(item.value)}
      />
      <p class="text-sm">Current section: {section()}</p>
    </div>
  )
}
```

### Custom states and navigation

Use the custom item renderer for a changed item layout, and provide loading or empty content when those states need explanation. Sub-navigation should represent a short, deliberate branch of commands rather than a complete application shell.

```tsx
import { CommandPalette } from 'moraine'

export function CustomStates() {
  return (
    <div class="b-1 b-border rounded-xl max-w-md w-full shadow-lg overflow-hidden">
      <CommandPalette
        autofocus={false}
        placeholder="Search empty query..."
        groups={[]}
        emptyRender={() => (
          <p class="text-xs text-muted-foreground p-6 text-center">
            No commands found matching your query.
          </p>
        )}
      />
    </div>
  )
}
```

### Keyboard interaction

| Key                              | Description                                                            |
| -------------------------------- | ---------------------------------------------------------------------- |
| <kbd>ArrowDown</kbd>             | Moves to the next available command.                                   |
| <kbd>ArrowUp</kbd>               | Moves to the previous available command.                               |
| <kbd>Enter</kbd>                 | Activates the current command.                                         |
| <kbd>Escape</kbd>                | Clears the current query or requests close according to current state. |
| <kbd>Home</kbd> / <kbd>End</kbd> | Moves to the first or last available command.                          |

## Examples

### Custom item rendering

```tsx
import { Badge, CommandPalette, Icon } from 'moraine'
import type { CommandPaletteT } from 'moraine'

interface TeamCommand extends CommandPaletteT.Item {
  owner: string
  status: 'ready' | 'blocked'
}

const GROUPS: CommandPaletteT.Group<TeamCommand>[] = [
  {
    id: 'projects',
    label: 'Projects',
    items: [
      {
        value: 'moraine-docs',
        label: 'Moraine Docs',
        description: 'Update examples for the next release',
        owner: 'Design Systems',
        status: 'ready',
      },
      {
        value: 'billing-api',
        label: 'Billing API',
        description: 'Waiting on staging credentials',
        owner: 'Platform',
        status: 'blocked',
      },
    ],
  },
]

export function CustomItemRender() {
  return (
    <div class="max-w-full w-lg">
      <CommandPalette<TeamCommand>
        groups={GROUPS}
        autofocus={false}
        itemRender={(ctx) => (
          <div class="flex flex-1 gap-3 min-w-0 items-center">
            <Icon name="i-lucide-folder-kanban text-muted-foreground group-data-[highlighted]:text-accent-foreground shrink-0" />
            <span class="flex flex-1 flex-col min-w-0">
              <span class="text-sm font-medium truncate">{ctx.item.label}</span>
              <span class="text-xs text-muted-foreground truncate group-data-[highlighted]:text-accent-foreground">
                {ctx.item.owner} · {ctx.item.description}
              </span>
            </span>
            <Badge variant={ctx.item.status === 'ready' ? 'subtle' : 'outline'}>
              {ctx.item.status}
            </Badge>
          </div>
        )}
      />
    </div>
  )
}
```

### Custom empty state

```tsx
import { CommandPalette, Icon } from 'moraine'

export function CustomEmptyState() {
  return (
    <div class="max-w-full w-lg">
      <CommandPalette
        groups={[]}
        autofocus={false}
        emptyRender={() => (
          <span class="flex flex-col gap-2 items-center">
            <Icon name="i-lucide-search-x" class="text-xl text-muted-foreground" />
            <span class="text-foreground font-medium">No commands found</span>
            <span class="text-xs">Try a different keyword or clear the search.</span>
          </span>
        )}
      />
    </div>
  )
}
```

### Loading

```tsx
import { CommandPalette, Icon, KbdGroup } from 'moraine'
import type { CommandPaletteT } from 'moraine'

export function Loading() {
  const BASIC_GROUPS: CommandPaletteT.Group[] = [
    {
      id: 'workspace',
      label: 'Workspace',
      items: [
        {
          value: 'new-issue',
          label: 'New Issue',
          leadingRender: () => <Icon name="i-lucide-circle-plus" />,
          trailingRender: () => <KbdGroup items={['⌘', 'N']} />,
        },
        {
          value: 'open-inbox',
          label: 'Open Inbox',
          leadingRender: () => <Icon name="i-lucide-inbox" />,
          trailingRender: () => <KbdGroup items={['⌘', 'I']} />,
        },
        {
          value: 'sync-roadmap',
          label: 'Sync Roadmap',
          leadingRender: () => <Icon name="i-lucide-refresh-cw" />,
          description: 'Pull the latest planning updates',
        },
      ],
    },
  ]

  return (
    <div class="max-w-full w-lg">
      <CommandPalette groups={BASIC_GROUPS} autofocus={false} loading />
    </div>
  )
}
```

### Sub-navigation

```tsx
import { Button, CommandPalette, Icon } from 'moraine'
import type { CommandPaletteT } from 'moraine'
import { createMemo, createSignal } from 'solid-js'

export function SubNavigation() {
  const ROOT_GROUPS: CommandPaletteT.Group[] = [
    {
      id: 'main',
      label: 'Commands',
      items: [
        {
          value: 'create',
          label: 'Create',
          leadingRender: () => <Icon name="i-lucide-plus-circle" />,
          description: 'Create new resources',
        },
        {
          value: 'share',
          label: 'Share',
          leadingRender: () => <Icon name="i-lucide-share-2" />,
          description: 'Share with others',
        },
        {
          value: 'delete',
          label: 'Delete',
          leadingRender: () => <Icon name="i-lucide-trash-2" />,
        },
      ],
    },
  ]
  const CREATE_GROUPS: CommandPaletteT.Group[] = [
    {
      id: 'create',
      label: 'Create',
      items: [
        {
          value: 'create-new-file',
          label: 'New File',
          leadingRender: () => <Icon name="i-lucide-file-plus" />,
        },
        {
          value: 'create-new-folder',
          label: 'New Folder',
          leadingRender: () => <Icon name="i-lucide-folder-plus" />,
        },
        {
          value: 'create-new-project',
          label: 'New Project',
          leadingRender: () => <Icon name="i-lucide-git-branch" />,
        },
      ],
    },
  ]
  const SHARE_GROUPS: CommandPaletteT.Group[] = [
    {
      id: 'share',
      label: 'Share',
      items: [
        {
          value: 'share-copy-link',
          label: 'Copy Link',
          leadingRender: () => <Icon name="i-lucide-link" />,
          trailingRender: () => <span class="text-xs text-muted-foreground">⌘L</span>,
        },
        {
          value: 'share-send-email',
          label: 'Send via Email',
          leadingRender: () => <Icon name="i-lucide-mail" />,
        },
      ],
    },
  ]
  const [view, setView] = createSignal<'root' | 'create' | 'share'>('root')

  const groups = createMemo(() => {
    switch (view()) {
      case 'create':
        return CREATE_GROUPS
      case 'share':
        return SHARE_GROUPS
      case 'root':
      default:
        return ROOT_GROUPS
    }
  })

  const onSelect = (item: CommandPaletteT.Item) => {
    if (item.value === 'create') {
      setView('create')
    } else if (item.value === 'share') {
      setView('share')
    }
  }

  return (
    <div class="flex flex-col gap-3 max-w-full w-lg">
      <div class="flex gap-3 items-center justify-between">
        <p class="text-sm text-muted-foreground">
          Drive multi-step navigation outside the component by swapping the `groups` prop.
        </p>
        <Button
          size="sm"
          variant="outline"
          disabled={view() === 'root'}
          onClick={() => setView('root')}
        >
          Back
        </Button>
      </div>
      <CommandPalette
        groups={groups()}
        autofocus={false}
        closeOnSelect={false}
        onSelect={onSelect}
      />
    </div>
  )
}
```

## Attributes

| Attributes | Slot | Description |
| --- | --- | --- |
| `data-disabled` | `command-palette-item` | Present when the component, slot, or item is disabled. |
| `data-highlighted` | `command-palette-item`, `command-palette-item-leading`, `command-palette-item-description`, `command-palette-item-trailing` | Present when the item is highlighted by pointer or keyboard navigation. |
| `data-loading` | `command-palette-input-leading` | Present when the component or async operation is loading. |

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| autofocus | boolean \| undefined | true | Whether to focus the search input automatically on mount. |
| closeIcon | IconT.Name \| undefined | 'icon-close' | Icon name for the palette close button. |
| closeOnSelect | boolean \| undefined | true | Whether to request closing the palette after an enabled item is selected. |
| descriptionPosition | 'bottom' \| 'trailing' \| undefined | 'bottom' | Where descriptions render in each command item. |
| disableFilter | boolean \| undefined | false | Disable built-in search filtering and render all provided items. |
| emptyRender | JSX.Element \| ((props: EmptyRenderProps<TItem>) => JSX.Element) \| undefined | — | Content or render function for the empty state. |
| filterItems | ((args: { groups: Group<TItem>[]; searchTerm: string }) => Group<TItem>[]) \| undefined | — | Custom filter function that fully controls which groups and items are visible. |
| footerRender | JSX.Element \| ((props: FooterRenderProps<TItem>) => JSX.Element) \| undefined | — | Content or render function for the footer. |
| getItemSearchText | ((item: TItem, group: Group<TItem>) => string) \| undefined | — | Custom search text builder for built-in filtering. |
| groups | ({<br>  /** Unique identifier for the group. */<br>  id: string;<br>  /** Display name for the group header. */<br>  label?: string;<br>  /** Items belonging to this group. */<br>  items?: ({<br>    /** Unique value for the item. */<br>    value: string;<br>    /** Primary label for the item. */<br>    label?: string;<br>    /** Secondary description text shown for the item. */<br>    description?: string;<br>    /** Additional keywords included in built-in search matching. */<br>    keywords?: string[];<br>    /** Content or render function at the start of this item. */<br>    leadingRender?: JSX.Element \| ((props: ItemRenderProps) => JSX.Element);<br>    /** Content or render function at the end of this item. */<br>    trailingRender?: JSX.Element \| ((props: ItemRenderProps) => JSX.Element);<br>    /** Whether the item is disabled and cannot be selected. */<br>    disabled?: boolean;<br>    /** Whether this item should be excluded from built-in search filtering. */<br>    alwaysShow?: boolean;<br>    /** Callback triggered when the item is selected. */<br>    onSelect?: () => void;<br>  })[];<br>})[] \| undefined | [] | Command groups to display initially. |
| inputProps | InputElementProps \| undefined | — | Additional attributes for the search input. |
| inputRef | Ref<HTMLInputElement> \| undefined | — | Ref forwarded to the inner search `<input>` element. |
| itemProps | ((context: ItemRenderProps<TItem>) => ElementProps<HTMLDivElement> \| undefined) \| undefined | — | Additional attributes for a command row. |
| itemRender | ((props: ItemRenderProps<TItem>) => JSX.Element) \| undefined | — | Renderer for each command row. |
| leadingIcon | IconT.Name \| undefined | 'icon-search' | Icon name of input's leading icon. |
| listboxProps | Omit<ElementProps<HTMLDivElement>, 'children'> \| undefined | — | Additional attributes for the command listbox. |
| loading | boolean \| undefined | false | Whether the palette is in a loading state. |
| loadingIcon | IconT.Name \| undefined | 'icon-loading' | Icon name of input's leading icon for the loading state. |
| onClose | (() => void) \| undefined | — | Callback triggered when the close button is clicked or selection requests closing. |
| onSearchTermChange | ((term: string) => void) \| undefined | — | Callback triggered when the search term changes. |
| onSelect | ((item: TItem) => void) \| undefined | — | Callback triggered when an enabled item is selected. |
| placeholder | string \| undefined | 'Search...' | Placeholder text for the search input. |
| ref | Ref<HTMLDivElement> \| undefined | — | Ref forwarded to the root `<div>` element. |
| scrollToItem | ((item: TItem, entryIndex: number) => void) \| undefined | — | Scrolls a highlighted command into view using its flattened entry index. |
| searchMaxLength | number \| undefined | — | Maximum allowed length for the search text. |
| searchTerm | string \| undefined | — | Controlled search term. |
| showClose | boolean \| undefined | false | Whether to show a close button in the header. |
| virtualRender | Component<VirtualRenderProps<TItem>> \| undefined | — | Renders flattened group labels and commands through a virtualization layer. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| classes | Classes \| undefined | — | Family slot class defaults for this instance. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
| styles | Styles \| undefined | — | Family slot style defaults for this instance. |
