Skip to main content
WireKit
Copy for LLM

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 with grouped results and keyboard navigation.

Usage

Command Palette

Basic Setup

<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:

{{-- 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:

<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

<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:

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

Programmatic Open/Close

<!-- 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
{{-- 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 (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

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:

{{-- 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>
// 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:

<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 Enter 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 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:

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

Scoped Personalization

<x-wirekit::command-palette scope="admin-search">
    ...
</x-wirekit::command-palette>
'personalizations' => [
    'command-palette' => [
        'admin-search' => [
            'base' => 'max-w-2xl',
        ],
    ],
],

Further Reading

Was this page helpful?

Voting requires cookies or local storage. What we store