---
title: Command Palette with Pages
description: A command palette whose entries open a list of values in the same palette, with a search of its own, Back, and the digits 1 to 9.
visibility: guest
draft: false
---

# Command Palette with Pages

A bulk action often needs one more choice: "Set status…" asks which status. An entry with `opens`
shows that choice as a page of the same [`<x-wirekit::command-palette>`](/components/command-palette):

- the field empties, and what the reader types searches the page,
- the values carry the digits 1 to 9, and a digit chooses one while the field is empty,
- Backspace in the empty field, or Back above it, goes up a level, and Escape closes the palette.

Open the demo, choose "Set status…" and press a digit. Type to narrow a page, and press Backspace
until you are back on the top level.

## The Demo

:::preview{title="An entry that opens a page of statuses"}
<div x-data style="height: 28rem; contain: layout; position: relative; --wk-command-palette-offset-top: 4rem;">
<x-wirekit::command-palette name="bulk-actions" named-only hotkey="" :teleport="false" :lock-scroll="false" label="Bulk actions">
    <x-wirekit::command-palette.item opens="status">Set status…</x-wirekit::command-palette.item>
    <x-wirekit::command-palette.item opens="label">Add label…</x-wirekit::command-palette.item>
    <x-wirekit::command-palette.item>Export</x-wirekit::command-palette.item>
    <x-wirekit::command-palette.page name="status" heading="Set status" placeholder="Search statuses…">
        <x-wirekit::command-palette.item>Active</x-wirekit::command-palette.item>
        <x-wirekit::command-palette.item>Draft</x-wirekit::command-palette.item>
        <x-wirekit::command-palette.item>Archived</x-wirekit::command-palette.item>
    </x-wirekit::command-palette.page>
    <x-wirekit::command-palette.page name="label" heading="Add label" placeholder="Search labels…">
        <x-wirekit::command-palette.item>Bestseller</x-wirekit::command-palette.item>
        <x-wirekit::command-palette.item>Clearance</x-wirekit::command-palette.item>
        <x-wirekit::command-palette.item>New arrival</x-wirekit::command-palette.item>
        <x-wirekit::command-palette.item>Seasonal</x-wirekit::command-palette.item>
    </x-wirekit::command-palette.page>
    <x-slot:empty>
        <x-wirekit::command-palette.empty>Nothing matches that search.</x-wirekit::command-palette.empty>
    </x-slot:empty>
</x-wirekit::command-palette>
<x-wirekit::center style="height: 100%">
<x-wirekit::button intent="neutral" surface="outline" @click="$dispatch('wirekit-command-palette-show', { name: 'bulk-actions' })">Open the actions</x-wirekit::button>
</x-wirekit::center>
</div>
:::

:::source{language="blade"}
{{-- 1. An entry with `opens` names the page it shows. --}}
<x-wirekit::command-palette name="bulk-actions" label="Bulk actions">
    <x-wirekit::command-palette.item opens="status">Set status…</x-wirekit::command-palette.item>
    <x-wirekit::command-palette.item wire:click="export">Export</x-wirekit::command-palette.item>

    {{-- 2. The page holds entries like any other, each with your action on it. --}}
    <x-wirekit::command-palette.page name="status" heading="Set status" placeholder="Search statuses…">
        <x-wirekit::command-palette.item wire:click="setStatus('active')">Active</x-wirekit::command-palette.item>
        <x-wirekit::command-palette.item wire:click="setStatus('draft')">Draft</x-wirekit::command-palette.item>
        <x-wirekit::command-palette.item wire:click="setStatus('archived')">Archived</x-wirekit::command-palette.item>
    </x-wirekit::command-palette.page>

    {{-- 3. Shown while a page's search matches nothing, as on the top level. --}}
    <x-slot:empty>
        <x-wirekit::command-palette.empty>Nothing matches that search.</x-wirekit::command-palette.empty>
    </x-slot:empty>
</x-wirekit::command-palette>
:::

A page stands anywhere inside the palette, before or after the entries that open it, and an entry
on a page can open another page: Back then walks the trail one level at a time. Choosing a value
runs its action and closes the palette, as any entry does, and the next open starts on the top
level.

## What the Page Does

| On a page | |
| --- | --- |
| The field | Empties when the page opens. The page's `placeholder` stands in it, the palette's own without one |
| Typing | Narrows the page's entries by their text, without case and accents: `entwurfe` finds "Entwürfe". A group whose entries all drop out hides with them |
| `1` – `9` | Choose the entry that carries the digit, while the field is empty. The first nine entries that can be chosen carry one, a disabled entry none. With text in the field, a digit is part of the search |
| `Backspace` | With text in the field, delete it. In the empty field, go back one level |
| Back | The button above the field goes back one level. It shows only while a page is open |
| `Escape` | Close the palette, from any level |

The top level keeps its own rule: the palette never filters it, and your search answers it, as
[Server-Side Search](/components/command-palette#server-side-search) describes.

## Values From Your Server

A page with `remote` takes its values from your search, the way the top level does. The palette
sends `wirekit-command-palette-query` with the page's name in `detail.page` as soon as the page
opens, with an empty query, and again as the reader types; on the top level `detail.page` is
`null`. Render the values into the page, and the palette shows them:

<!-- markdownlint-disable MD031 -->
:::codegroup
```blade title="Blade view"
{{-- 1. One handler answers the top level and the remote page: `page` says which. --}}
<x-wirekit::command-palette name="bulk-actions" label="Bulk actions"
    x-on:wirekit-command-palette-query="$wire.search($event.detail.page, $event.detail.query)">
    <x-wirekit::command-palette.item opens="category">Assign category…</x-wirekit::command-palette.item>

    {{-- 2. `remote`: the component renders the page's values for its query. --}}
    <x-wirekit::command-palette.page name="category" heading="Assign category" remote>
        @foreach ($categories as $category)
            <x-wirekit::command-palette.item :id="'category-' . $category->id" wire:key="category-{{ $category->id }}" wire:click="assignCategory({{ $category->id }})">
                {{ $category->name }}
            </x-wirekit::command-palette.item>
        @endforeach
    </x-wirekit::command-palette.page>
</x-wirekit::command-palette>
```

```php title="Livewire component"
// 3. The query of the category page narrows its values; the top level has none here.
public string $categorySearch = '';

public function search(?string $page, string $query): void
{
    if ($page === 'category') {
        $this->categorySearch = $query;
    }
}

// 4. Nine values at most, so each one carries a digit.
public function render()
{
    return view('livewire.product-list', [
        'categories' => Category::query()
            ->when($this->categorySearch !== '', fn ($query) => $query->where('name', 'like', '%'.$this->categorySearch.'%'))
            ->orderBy('name')
            ->limit(9)
            ->get(),
    ]);
}
```
:::
<!-- markdownlint-enable MD031 -->

A page your server answers shows its `loading` and `error` slots while you report the request
through `wirekit:command-palette-state`, as
[Command Palette with a Remote Search](/components/command-palette/remote-search) shows. Its count is
announced once you report the answer, or once its values arrive.

## Events

| Event | Detail | When |
| --- | --- | --- |
| `wirekit-command-palette-query` | `query`, `page` | On every keystroke on the top level and on a remote page, debounced, and once when such a level opens. A page the palette filters sends none |
| `wirekit:command-palette-page` | `page`, `name` | When the reader opens a page or goes back from one. `page` is `null` on the top level, `name` the palette's own |
| `wirekit:command-palette-submit` | `query`, `page` | On Enter with nothing highlighted, on any level |

## Accessibility

- A page is a `role="group"` inside the listbox, named by its `heading`, and only the open level is
  in the accessibility tree.
- Each change of level is said through the palette's status region: the level's name and how many
  entries it holds, "Set status, 3 entries". The region is in the page before its first sentence,
  so a screen reader speaks it.
- The field keeps the focus on every change of level, so the arrows, Enter and the digits stay
  where the reader is. Back is a button before the field, reached with `Shift+Tab`.
- The chevron of an entry with `opens` and the digit beside a value are hidden from screen readers;
  the entry's text is its name. The digit is a key the palette binds, so the entry states it as
  `aria-keyshortcuts` while it carries one, beside an `aria-keyshortcuts` of your own.

## Props

The props of `<x-wirekit::command-palette.page>` and the `opens` prop of
`<x-wirekit::command-palette.item>` are listed with the others on the
[Command Palette](/components/command-palette#props) page.
