---
title: Command Palette
description: Searchable command palette (⌘K)
visibility: guest
draft: false
---

# Command Palette

The `<x-wirekit::command-palette>` component creates a spotlight-style search modal activated by a keyboard shortcut (Cmd+K / Ctrl+K by default). It implements the [WAI-ARIA Combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) with grouped results and keyboard navigation.

## Usage

:::preview{title="Command Palette"}
<div x-data style="height: 28rem; contain: layout; position: relative; --wk-command-palette-offset-top: 4rem;">
<x-wirekit::command-palette :teleport="false" :lock-scroll="false">
    <x-wirekit::command-palette.group heading="Navigation">
        <x-wirekit::command-palette.item href="/dashboard" icon="user">Dashboard</x-wirekit::command-palette.item>
        <x-wirekit::command-palette.item href="/projects" icon="edit">Projects</x-wirekit::command-palette.item>
    </x-wirekit::command-palette.group>
    <x-wirekit::command-palette.group heading="Settings">
        <x-wirekit::command-palette.item href="/settings/profile" icon="user">Profile</x-wirekit::command-palette.item>
        <x-wirekit::command-palette.item href="/settings/appearance" icon="eye">Appearance</x-wirekit::command-palette.item>
    </x-wirekit::command-palette.group>
</x-wirekit::command-palette>
<x-wirekit::center style="height: 100%">
<x-wirekit::button intent="neutral" surface="outline" @click="$dispatch('wirekit-command-palette-show')">Open Command Palette</x-wirekit::button>
</x-wirekit::center>
</div>
:::

:::source{language="blade"}
{{-- The preview above is boxed into a fixed-height frame and opts out of
     teleporting so it can render inside the page. Yours needs neither: the
     defaults teleport the panel to the body and lock background scroll. --}}
<x-wirekit::command-palette>
    <x-wirekit::command-palette.group heading="Navigation">
        <x-wirekit::command-palette.item href="/dashboard" icon="user">Dashboard</x-wirekit::command-palette.item>
        <x-wirekit::command-palette.item href="/projects" icon="edit">Projects</x-wirekit::command-palette.item>
    </x-wirekit::command-palette.group>
    <x-wirekit::command-palette.group heading="Settings">
        <x-wirekit::command-palette.item href="/settings/profile" icon="user">Profile</x-wirekit::command-palette.item>
        <x-wirekit::command-palette.item href="/settings/appearance" icon="eye">Appearance</x-wirekit::command-palette.item>
    </x-wirekit::command-palette.group>
</x-wirekit::command-palette>

<x-wirekit::button intent="neutral" surface="outline"
    @click="$dispatch('wirekit-command-palette-show')">
    Open Command Palette
</x-wirekit::button>
:::

## Basic Setup

```blade
<x-wirekit::command-palette>
    <x-wirekit::command-palette.group heading="Navigation">
        <x-wirekit::command-palette.item href="/dashboard" icon="user">
            Dashboard
        </x-wirekit::command-palette.item>
        <x-wirekit::command-palette.item href="/projects" icon="edit">
            Projects
        </x-wirekit::command-palette.item>
    </x-wirekit::command-palette.group>

    <x-wirekit::command-palette.group heading="Settings">
        <x-wirekit::command-palette.item href="/settings/profile" icon="user">
            Profile
        </x-wirekit::command-palette.item>
        <x-wirekit::command-palette.item href="/settings/appearance" icon="eye">
            Appearance
        </x-wirekit::command-palette.item>
    </x-wirekit::command-palette.group>

    <x-wirekit::command-palette.empty>
        No results found.
    </x-wirekit::command-palette.empty>

    <x-slot:footer>
        <span>Navigate with arrow keys</span>
    </x-slot:footer>
</x-wirekit::command-palette>
```

## Custom Hotkey

Change the keyboard shortcut that opens the palette:

```blade
{{-- Default: Cmd+K (macOS) / Ctrl+K (Windows/Linux) --}}
<x-wirekit::command-palette hotkey="cmd+k">

{{-- Alternative: Ctrl+/ --}}
<x-wirekit::command-palette hotkey="ctrl+/">

{{-- Forward slash only (like GitHub) --}}
<x-wirekit::command-palette hotkey="/">
```

## Items with Shortcuts

Display keyboard shortcuts alongside items for discoverability:

```blade
<x-wirekit::command-palette.item href="/new" icon="plus" shortcut="Ctrl+N">
    New Project
</x-wirekit::command-palette.item>

<x-wirekit::command-palette.item href="/search" icon="search" shortcut="Ctrl+F">
    Search Files
</x-wirekit::command-palette.item>
```

## Disabled Items

```blade
<x-wirekit::command-palette.item :disabled="true" icon="download">
    Export (coming soon)
</x-wirekit::command-palette.item>
```

Disabled items are visually muted, skipped by keyboard navigation, and have `aria-disabled="true"`.

## Empty State

The `command-palette.empty` sub-component is shown when the search query matches no items:

```blade
<x-wirekit::command-palette.empty>
    No results found. Try a different search term.
</x-wirekit::command-palette.empty>
```

## Programmatic Open/Close

```blade
<!-- Alpine -->
<button x-on:click="$dispatch('wirekit-command-palette-show')">Open Search</button>
<button x-on:click="$dispatch('wirekit-command-palette-close')">Close Search</button>

<!-- Livewire (server-side) -->
$this->dispatch('wirekit-command-palette-show');
```

## Width & Layout

The command palette has a fixed max-width of 32rem (512px) and is centered near the top of the viewport — following the established macOS Spotlight pattern.

The results list grows with content up to **18rem (288px)** and scrolls beyond that. Two variables move it, and neither needs a build step:

| Variable | Default | What it moves |
| --- | --- | --- |
| `--wk-command-palette-list-max-height` | `18rem` | How tall the results list gets before it scrolls |
| `--wk-command-palette-offset-top` | — | How far below the top of the viewport the panel sits |

```blade
{{-- 1. A palette showing ranked search results wants more room than a short
        command list does — roughly half the viewport reads well. --}}
<div style="--wk-command-palette-list-max-height: 50vh;">
    <x-wirekit::command-palette>
        {{-- items --}}
    </x-wirekit::command-palette>
</div>
```

Set it on any ancestor; it inherits. For a whole-application change, put it on `:root` in your stylesheet.

## Behavior

- **Keyboard shortcut** listener is registered globally on `document` and respects modifier keys
- **[Focus trap](https://github.com/focus-trap/focus-trap)** (bundled ~3.8 KB) keeps keyboard navigation within the palette
- **Scroll lock** hides body scrollbar while the palette is open
- **Teleport** renders the palette at `<body>` level for correct stacking
- **You own the result set** -- the palette renders exactly the items you pass and does **not** filter them itself. Wire the search query to your own filtering (server-side or client-side) via the query event below
- **Livewire SPA** navigation (`wire:navigate`) automatically closes open palettes
- **Transitions** use scale + opacity for smooth open/close animation

## Server-Side Search

The palette renders whatever items you give it — it never filters them itself.
To drive a live, server-backed search, listen for the `wirekit-command-palette-query`
event: it fires on every keystroke (debounced 300 ms) and carries the current
query in `event.detail.query`. Feed that query to Livewire, re-query your data,
and re-render the item list:

```blade
{{-- 1. Listen on the palette itself — the event is dispatched from its root. --}}
<x-wirekit::command-palette x-on:wirekit-command-palette-query="$wire.set('search', $event.detail.query)">
    {{-- 2. Render the server-filtered results. Livewire swaps this list as `search` changes.
            Give each item a stable id so the list keeps its identity across re-renders. --}}
    @foreach ($results as $result)
        <x-wirekit::command-palette.item :id="'cmd-' . $result->id" :href="$result->url">
            {{ $result->title }}
        </x-wirekit::command-palette.item>
    @endforeach
</x-wirekit::command-palette>
```

```php
// 3. In the Livewire component: re-query whenever `search` updates.
public string $search = '';

public function getResultsProperty(): Collection
{
    // Debounced upstream by the event, so this runs at most ~3×/second while typing.
    return Command::search($this->search)->take(20)->get();
}
```

From a Livewire component, use the **full** event name — Livewire rewrites
`wire:<event>` to `x-on:<event>` verbatim, so the `wirekit-` prefix has to be
there:

```blade
<x-wirekit::command-palette wire:wirekit-command-palette-query="search($event.detail.query)" />
```

The event bubbles, so a listener on any ancestor works too — but the palette
itself is the natural place, and it is where it now fires from.

### Keeping the list stable while it re-renders

Server-side search rebuilds the item list on every keystroke, which puts two
things at risk. Both are handled, and one needs something from you:

- **Item identity — pass an `id`.** The item id anchors the combobox's
  `aria-activedescendant`. Derived automatically from the item's `href`, or its
  text when there is no href; pass `:id` explicitly when you have a real key.
  Without a stable id the screen reader's announcement target moves on every
  keystroke and Livewire's morph patches every row.
- **Keyboard selection.** When the results change, the highlight is cleared
  rather than left pointing at whatever now occupies that position — so
  <kbd>Enter</kbd> can never activate a row the reader never saw. When the same
  results merely re-render, the highlight is restored (morph strips it, because
  the highlight lives only in the browser).

## Props

### `<x-wirekit::command-palette>`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `hotkey` | `string` | `'cmd+k'` | Keyboard shortcut to open the palette |
| `placeholder` | `string` | `'Search commands...'` | Placeholder text for the search input |
| `teleport` | `bool` | `true` | Teleport the overlay to `<body>` so it sits above every stacking context. Set to `false` to render the overlay inline — required when embedding the palette inside a scoped container (e.g. docs preview cards with `contain: layout`) |
| `lockScroll` | `bool` | `true` | Lock `document.body` scroll while the palette is open (standard modal behavior). Set to `false` when the palette is embedded inside a local container and you want the rest of the page to stay scrollable and interactive |
| `scope` | `string\|null` | `null` | Scoped personalization key |

### `<x-wirekit::command-palette.group>`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `heading` | `string` | **required** | Group label displayed above items |

### `<x-wirekit::command-palette.item>`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `href` | `string\|null` | `null` | URL (renders `<a>` instead of `<button>`) |
| `icon` | `string\|null` | `null` | WireKit icon alias (e.g. `'search'`, `'user'`) |
| `shortcut` | `string\|null` | `null` | Keyboard shortcut hint text (display only) |
| `disabled` | `bool` | `false` | Disabled state |
| `id` | `string\|null` | `null` | Stable DOM id anchoring `aria-activedescendant`. Derived from `href`, else the item text, else random — pass one when you have a real key and the list re-renders. |

### Slots

| Slot | Purpose |
| --- | --- |
| default | Groups and items |
| `footer` | Optional footer with keyboard hints or actions |

## Sub-Components

| Component | Purpose |
| --- | --- |
| `command-palette.group` | Groups related items under a heading |
| `command-palette.item` | A single command entry (link or action) |
| `command-palette.empty` | Shown when no items match the search query |

## Accessibility

- **ARIA pattern:** Implements the [WAI-ARIA Combobox](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) with `role="combobox"` on the search input and `role="listbox"` on the results list
- **Input:** `aria-autocomplete="list"`, `aria-expanded`, `aria-controls` (linked to listbox ID)
- **Active descendant:** `aria-activedescendant` on the input tracks the currently highlighted option
- **Groups:** `role="group"` with `aria-label` from the `heading` prop
- **Items:** `role="option"`, `aria-selected` on the active item
- **Disabled items:** `aria-disabled="true"`, skipped by arrow key navigation
- **Focus trap** active while open -- Tab cycles within the palette
- **Focus returns** to the previously focused element on close
- **Overlay:** `role="dialog"`, `aria-modal="true"`, `aria-label="Command palette"`
- **Live region:** `aria-live="polite"` announces result count changes to screen readers
- Icons: `aria-hidden="true"` (decorative)

## Keyboard Interaction

| Key | Action |
|-----|--------|
| `Cmd+K` / `Ctrl+K` (configurable via `hotkey`) | Open the palette |
| Type to search | Filter commands |
| `ArrowUp` / `ArrowDown` | Move between results |
| `Home` / `End` | Jump to first / last result |
| `Enter` | Run the focused command (navigate or trigger its action) |
| `Escape` | Close the palette, return focus to the previously focused element |

## Pitfalls

- **A palette item is a plain link or button — there is no `:on-select` callback prop.** Each item renders as an `<a href>` (when you pass `href`) or a `<button>`. Selecting an item is the native activation: a click, or Enter on the focused item. Navigate by giving the item an `href`; for an in-app action, make the item a `<button>` and drive the behavior from its own native activation rather than layering a `wire:click` on top, which can race the palette's close-on-select Alpine handler and leave it open.
- **Don't put more than ~50 items in the default view.** Filter to <10 results via the search field, or paginate by category. WAI-ARIA Listbox Pattern requires every item be in the keyboard tab order — long lists become unusable.

## Design Tokens

| Element | Token |
| --- | --- |
| Overlay background | `--color-wk-overlay` |
| Panel background | `--color-wk-bg-elevated` |
| Panel border | `--color-wk-border` / `--border-wk-width` |
| Panel radius | `--radius-wk-xl` |
| Panel shadow | `--shadow-wk-lg` |
| Search input text | `--color-wk-text` |
| Search placeholder | `--color-wk-text-placeholder` |
| Group heading color | `--color-wk-text-muted` |
| Item text | `--color-wk-text` |
| Item hover/active bg | `--color-wk-bg-subtle` |
| Item icon color | `--color-wk-text-muted` |
| Shortcut badge border | `--color-wk-border` |
| Shortcut badge radius | `--radius-wk-sm` |
| Footer border | `--color-wk-border-subtle` |
| Font family | `--font-wk-sans` |
| Font size | `--text-wk-md` |
| Transition | `--transition-wk-duration` |
| Disabled opacity | `--opacity-wk-disabled` |
| Results list max height | `--wk-command-palette-list-max-height` (default `18rem`) |
| Panel offset from top | `--wk-command-palette-offset-top` |

## Personalization

Override defaults in `config/wirekit.php`:

```php
'components' => [
    'command-palette' => [
        'hotkey' => 'ctrl+/',
        'placeholder' => 'Type a command...',
    ],
],
```

### Scoped Personalization

```blade
<x-wirekit::command-palette scope="admin-search">
    ...
</x-wirekit::command-palette>
```

```php
'personalizations' => [
    'command-palette' => [
        'admin-search' => [
            'base' => 'max-w-2xl',
        ],
    ],
],
```

## Further Reading

- [WAI-ARIA Combobox Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) -- the authoring pattern this component implements
- [WAI-ARIA Listbox Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/) -- used for the results list
- [MDN: `role="combobox"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/combobox_role)
- [MDN: `aria-activedescendant`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-activedescendant)
- [focus-trap](https://github.com/focus-trap/focus-trap) -- focus management library (bundled, ~3.8 KB)
