---
title: Command Palette with a Remote Search
description: A command palette that asks a server for its results, with a filter row between the input and the list, and a loading and an error state while the request is out.
visibility: guest
draft: false
---

# Command Palette with a Remote Search

This page builds a [`<x-wirekit::command-palette>`](/components/command-palette) whose results
come from a server.

A search that asks a server has two answers the empty state cannot give: the results are not
here yet, or the request failed. And the controls that narrow such a search belong above the
results, not beneath them. Three slots cover both:

- `filters` renders between the input and the list. <kbd>Tab</kbd> moves from the input into it.
- `loading` shows while your request is out.
- `error` shows when it failed.

The palette cannot see your request, so you report where it stands with the
`wirekit:command-palette-state` event. Open the demo and type: the loading line shows until
the answer arrives. Switch the section to narrow the list, and type `offline` to see the
failure. The demo only pretends to ask a server; Show Code has the markup of a real search,
and the factory it calls follows below.

## The Demo

:::preview{title="A remote search with a filter row, a loading state and an error state"}
<div x-data="{ section: 'all', failWith: 'offline' }" x-on:wirekit-command-palette-query.debounce.700ms="$dispatch('wirekit:command-palette-state', { state: $event.detail.query.trim().toLowerCase() === failWith ? 'error' : 'idle' })" style="height: 28rem; contain: layout; position: relative; --wk-command-palette-offset-top: 4rem;">
<x-wirekit::command-palette :teleport="false" :lock-scroll="false" x-on:wirekit-command-palette-query="$dispatch('wirekit:command-palette-state', { state: $event.detail.query === '' ? 'idle' : 'loading' })">
    <template x-if="section !== 'blueprints'">
        <x-wirekit::command-palette.group heading="Docs">
            <x-wirekit::command-palette.item id="cmd-remote-installation">Installation</x-wirekit::command-palette.item>
            <x-wirekit::command-palette.item id="cmd-remote-theming">Theming</x-wirekit::command-palette.item>
            <x-wirekit::command-palette.item id="cmd-remote-events">Events</x-wirekit::command-palette.item>
        </x-wirekit::command-palette.group>
    </template>
    <template x-if="section !== 'docs'">
        <x-wirekit::command-palette.group heading="Blueprints">
            <x-wirekit::command-palette.item id="cmd-remote-pricing">Pricing page</x-wirekit::command-palette.item>
            <x-wirekit::command-palette.item id="cmd-remote-settings">Settings page</x-wirekit::command-palette.item>
            <x-wirekit::command-palette.item id="cmd-remote-kpi">Live KPI strip</x-wirekit::command-palette.item>
        </x-wirekit::command-palette.group>
    </template>
    <x-slot:filters>
        <x-wirekit::segmented-control name="search-section" aria-label="Search in" size="sm" value="all" :options="['all' => 'All', 'docs' => 'Docs', 'blueprints' => 'Blueprints']" x-on:input="section = $event.target.value" />
    </x-slot:filters>
    <x-slot:loading>Searching…</x-slot:loading>
    <x-slot:error>The search is unavailable right now. Try again in a moment.</x-slot:error>
    <x-slot:empty>
        <x-wirekit::command-palette.empty>Nothing matches that search.</x-wirekit::command-palette.empty>
    </x-slot:empty>
    <x-slot:footer>
        <span>Type <kbd>offline</kbd> to see the error state</span>
    </x-slot:footer>
</x-wirekit::command-palette>
<x-wirekit::center style="height: 100%">
<x-wirekit::button intent="neutral" surface="outline" @click="$dispatch('wirekit-command-palette-show')">Open the search</x-wirekit::button>
</x-wirekit::center>
</div>
:::

:::source{language="blade"}
{{-- 1. The search lives in the factory registered below. The directives only
        call its methods, so the same markup runs under Alpine's CSP build. --}}
<div x-data="siteSearch">
    {{-- 2. The palette announces every keystroke (debounced) with the query event. --}}
    <x-wirekit::command-palette x-on:wirekit-command-palette-query="search($event.detail.query)">
        {{-- 3. Render the answer as options. `href="#"` makes each one a real link,
                the binding sets where it goes, and a stable id keeps the highlight
                on its row when the list re-renders. --}}
        <template x-for="result in results" :key="result.id">
            <x-wirekit::command-palette.item href="#" x-bind:href="result.url" x-bind:id="'search-' + result.id">
                <span x-text="result.title"></span>
            </x-wirekit::command-palette.item>
        </template>

        {{-- 4. The filter row, between the input and the list. --}}
        <x-slot:filters>
            <x-wirekit::segmented-control name="search-section" aria-label="Search in" size="sm" value="all"
                :options="['all' => 'All', 'docs' => 'Docs', 'blueprints' => 'Blueprints']"
                x-on:input="narrow($event.target.value)" />
        </x-slot:filters>

        {{-- 5. Plain text: the palette announces both through a region of its own. --}}
        <x-slot:loading>Searching…</x-slot:loading>
        <x-slot:error>The search is unavailable right now. Try again in a moment.</x-slot:error>

        <x-slot:empty>
            <x-wirekit::command-palette.empty>Nothing matches that search.</x-wirekit::command-palette.empty>
        </x-slot:empty>
    </x-wirekit::command-palette>
</div>
:::

## The Search Factory

The factory holds the request. A newer query aborts the older one, so a slow answer never
overwrites a fast one, and a failure is reported, so the error slot shows instead of "nothing
matched". Register it before Alpine starts:

```js
// 1. In a script loaded with `defer`, so it runs before Alpine starts.
document.addEventListener('alpine:init', () => {
    Alpine.data('siteSearch', () => ({
        section: 'all',
        term: '',
        results: [],
        controller: null,

        // 2. Ask the server for one query. A newer query aborts this one.
        async search(term) {
            this.term = term;
            this.controller?.abort();
            this.controller = new AbortController();
            this.$dispatch('wirekit:command-palette-state', { state: 'loading' });

            try {
                const url = '/search?q=' + encodeURIComponent(term) + '&section=' + this.section;
                const response = await fetch(url, { signal: this.controller.signal });

                if (! response.ok) {
                    throw new Error('Search answered ' + response.status);
                }

                this.results = await response.json();
                this.$dispatch('wirekit:command-palette-state', { state: 'idle' });
            } catch (error) {
                // 3. A request a newer query aborted reports nothing.
                if (error.name === 'AbortError') {
                    return;
                }

                this.results = [];
                this.$dispatch('wirekit:command-palette-state', { state: 'error' });
            }
        },

        // 4. A new section asks again for the same words.
        narrow(section) {
            this.section = section;
            this.search(this.term);
        },
    }));
});
```

## Reporting Where the Request Stands

The event takes one of three states:

| `state` | What the palette does |
| --- | --- |
| `loading` | Shows the `loading` slot, marks the list `aria-busy="true"`, and hides the empty state |
| `error` | Shows the `error` slot and hides the empty state |
| `idle` | Shows neither. The empty state is back to showing whenever the list holds no option |

Any other value counts as `idle`, so a palette never stays in the loading state because of an
unexpected word. The event is page-wide, like `wirekit-command-palette-show`: dispatch it
without naming a palette. Opening the palette resets the state to `idle` before it announces
the empty query, so a request you start in answer to that query keeps its `loading`.

From a Livewire component, dispatch the same event with named arguments:

```php
// 1. The request failed on the server — show the error slot.
$this->dispatch('wirekit:command-palette-state', state: 'error');
```

## The Three Slots

A few things to know about them:

- **`loading` and `error` take plain content.** The palette announces both through a
  `role="status"` region of its own, which is always in the page so a change inside it is
  read out. A `spinner` or anything else with its own `role="status"` inside it would be
  announced twice.
- **Put mutually exclusive filters in a `segmented-control`.** It is one <kbd>Tab</kbd> stop,
  and the arrow keys switch the section.
- **The list keys belong to the input.** <kbd>ArrowUp</kbd>, <kbd>ArrowDown</kbd>,
  <kbd>Home</kbd>, <kbd>End</kbd> and <kbd>Enter</kbd> move through the results only while
  the input has focus. A control in the filter row or the footer keeps its own keys.

## Further Reading

- [Command Palette](/components/command-palette) — every prop, slot and keyboard interaction of the component.
- [Overlay Events](/overlays/events) — the palette's show, close, query and state events beside the other overlays.
