Skip to content

[6.x] Command palette - #20081

Merged
brandonkelly merged 64 commits into
6.xfrom
feature/command-palette
Oct 28, 2026
Merged

brandonkelly merged 64 commits into
6.xfrom
feature/command-palette

Conversation

@brandonkelly

@brandonkelly brandonkelly commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Description

Adds a command palette/global search to the control panel. Activated via a search button in the global header, or with a ⌘/Ctrl + K keyboard shortcut.

Overview

  • The page's own results and the actions loaded with the palette show immediately. Server results fill in as each provider responds.
  • Results about the current page are listed first, however closely others match.
  • ↑/↓ move between results and Return opens one. Tab cycles between the search box, the selected result and its menu button. Esc clears the search, or closes the palette once it's empty; an open menu closes first, and a view goes back to the results. The footer shows these keys.
  • Choosing an item shows its view, or runs it, or runs its declarative action, or goes to its URL. That's the first of these it has.
  • An action with a confirm message asks inside the palette. Destructive actions are styled as dangerous and sorted last.
  • With an empty search box, the palette lists recently visited pages, then the actions for the current page.

Commands in core

On this page: found in the page itself, and listed first
  • Fields: by their labels, including native and custom fields. Choosing one opens its tab or details panel, scrolls to it and focuses its input.
    • Results show where each field sits, e.g. Content → Content Blocks → Exhibition Location.
    • Editable table columns and fields without a visible label are left out.
  • Matrix blocks: nested entries, by their UI labels, with their entry types' icons and colors.
  • Tabs, details panels and headings.
  • Landmarks: main content, navigation landmarks and breadcrumbs. Choosing one moves focus to it, as a skip link does.
  • Secondary navigation: e.g. a user's Permissions and Addresses.
Page actions: depend on the page being viewed
  • The items of the page's own action menus (View, Validate, Copy URL, etc.). Choosing one clicks it, prompts included.
  • Element editor:
    • The primary save action and the alternate form actions.
    • Discard changes.
    • Add to {release}, and Add to a new release.
    • Copy ID, Copy reference tag, and Copy URL.
  • Revisions: Go to the current revision, each draft, recent revisions, and View all revisions.
  • Switch to {site}, on multi-site pages.
  • Expand and collapse all blocks in {Matrix field}.
  • Expand or collapse the sidebar, and the global navigation.
  • Notifications, which opens a view listing the user’s notifications, as the global header’s notification center shows them. They come with the page, so the view opens without a request, and one marked as read in either place shows as read in both.
  • Keyboard shortcuts, which opens a view listing them.
  • Search Craft CMS documentation, which opens a view that searches the docs (see Search).
Pages
  • Global navigation: every item, flattened, and described by where it lives (e.g. “Sections”, in “Settings”).
  • My Account: each of its screens.
  • Front end: each site's base URL.
  • Settings edit pages (admins only), shown as “{name} settings”:
    • Sites, user groups, sections, entry types, workflows.
    • Asset volumes, image transforms, asset transformers, fields.
Actions
  • Content:
    • Create {section} entry, for each section the user can create entries in.
    • Create user.
  • Releases:
    • Create content release.
    • Publish {release}, which opens a view listing the release's drafts, as on its page. Drafts can be removed there, and Publish stays disabled, with a tooltip saying why, while any of them is blocked.
    • Schedule {release}, and Delete {release}.
  • Utilities:
    • Clear all caches, and Clear {cache} for each cache option.
    • Run pending queue jobs, Retry all failed jobs, and Release all jobs.
    • Rebuild project config, Check for updates, and Update asset indexes.
  • Preferences:
    • Set theme to Light, Dark or System.
    • Set language to {language}, and Set formatting locale to {locale}. These only appear in search.
    • Set week start day to {day}.
    • Show or hide field handles in edit forms (admins).
  • Settings (admins; everything but maintenance mode needs allowAdminChanges):
    • Create a site, route, user group, section, entry type, workflow, asset volume, image transform, asset transformer or field.
    • Delete the {name} user group, section, entry type, workflow, volume, image transform, asset transformer or field. These only appear in search.
    • Install, enable, disable and uninstall each plugin, and enable or disable all plugins.
    • Enable or disable maintenance mode.
  • Sign out.

Everything is gated on the same permissions and edition checks as the screens it stands in for.

Search
  • Entries, assets, and users that the user can view, each type with its own provider. Each result's menu offers the element's own actions, run the same way element chips run them.
  • Releases by name, published ones included and unpublished ones first, with their status and date.
  • Craft CMS documentation, from its own view rather than the palette's search, so only what's typed there is sent to craftcms.com. It searches the docs, Knowledge Base and glossary together, keeps only this version of Craft's docs, shows titles' and summaries' inline formatting such as code, and opens results in a new tab.

Extending it from plugins

Server side. Every item is a CommandPaletteItem:

use CraftCms\Cms\Cp\CommandPalette\CommandPaletteItem;

new CommandPaletteItem()
    ->id('my-plugin-clear-caches')       // stable; recents are remembered by it
    ->label('Clear My Plugin caches')
    ->description('My Plugin')           // secondary text, e.g. where it lives
    ->hint('…')                          // supporting text below, searched too
    ->labelHtml('…')->hintHtml('…')      // inline formatting, such as <code>; must be safe
    ->icon('broom')                      // or ->iconSvg(), ->thumbHtml(), ->iconColor()
    ->keywords('purge flush')            // hidden search terms
    // what choosing it does, first match wins:
    ->view(['type' => 'ui', 'modalUrl' => …, 'actionUrl' => …])
    ->action(['type' => 'http', 'url' => cp_url('my-plugin/clear-caches'), 'confirm' => 'Sure?'])
    ->href(cp_url('my-plugin/reports'))  // ->external() leaves the CP
    // options:
    ->variant('danger')                  // destructive; sorted last
    ->reload()                           // reload the page after the action
    ->removesPage($editUrl, $indexUrl)   // leave a page the action deletes
    ->searchOnly()                       // only when searched for, for big sets
    ->actions([...]);                    // secondary actions in a menu

Add pages and actions for the current user, from a service provider's boot():

use CraftCms\Cms\Cp\CommandPalette\ActionRegistry;
use CraftCms\Cms\Cp\CommandPalette\PageRegistry;

public function boot(PageRegistry $pages, ActionRegistry $actions): void
{
    $pages->register('my-plugin', fn (CraftUser $user) => $user->can('accessPlugin-my-plugin') ? [
        new CommandPaletteItem()
            ->id('my-plugin-reports')
            ->label('Reports')
            ->description('My Plugin')
            ->href(cp_url('my-plugin/reports')),
    ] : []);

    // Only add actions the user may run; the endpoints must still check.
    $actions->register('my-plugin', fn (CraftUser $user) => $user->isAdmin() ? [
        new CommandPaletteItem()
            ->id('my-plugin-clear-caches')
            ->label('Clear My Plugin caches')
            ->icon('broom')
            ->action(['type' => 'http', 'url' => cp_url('my-plugin/clear-caches')]),
    ] : []);
}

Each closure also gets any other dependencies it type-hints. remove('handle') takes one back out.

An http action's response can carry messages to show and a redirect to follow, as asSuccess() provides.

Add a search provider. The palette queries each available provider in parallel as the user types, then ranks everything together, so a provider only has to find matches:

use CraftCms\Cms\Cp\CommandPalette\SearchProviderRegistry;
use CraftCms\Cms\Cp\CommandPalette\SearchProviders\ElementSearchProvider;

public function boot(SearchProviderRegistry $searchProviders): void
{
    $searchProviders->register(
        // A custom element type, permission-checked per element:
        new ElementSearchProvider(
            Product::class,
            fn (CraftUser $user) => $user->can('commerce-manageProducts'),
        ),
        // Or anything else. A class name is resolved from the container each time.
        // implements SearchProvider: getHandle(), getLabel(), isAvailable(CraftUser),
        // search(string $query, CraftUser): iterable<CommandPaletteItem>
        MySearchProvider::class,
    );

    // Craft's own can be removed by handle, e.g. 'releases'.
    $searchProviders->remove('elements-user');
}

Client side, through Craft.commandPalette:

Craft.commandPalette.open();
Craft.commandPalette.close();

// Actions that depend on the current page. Called each time the palette
// opens; `unregister()` removes them again.
const {unregister} = Craft.commandPalette.registerActions(() => [
  {id: 'my-plugin-refresh', label: 'Refresh the report', description: 'This page', icon: 'arrows-rotate', run: () => report.refresh()},
]);

// A provider that searches in the browser. `displayFirst: true` lists its
// results before everyone else's, so it should only return close matches.
Craft.commandPalette.registerProvider({
  handle: 'my-plugin-widgets',
  label: 'Widgets',
  displayFirst: true,
  search: (query) => findWidgets(query),
});

// A custom view an item's `view: {type: 'my-plugin-picker', …}` opens.
// It gets `item` and `view` props, and `actionsTarget`, a selector for the
// view header's actions, beside its heading, to put its own buttons in with
// `<Teleport defer :to="actionsTarget">`. It emits `done` (optionally with
// `{message, run, redirect, reload}`) or `back`.
Craft.commandPalette.registerView('my-plugin-picker', PickerView);

A page's own action menu shows up as palette actions when it sits inside an element with data-command-palette-actions.

Client items take the same shape as server items, plus run(), and confirm to ask first.

Screenshots

Default state
The palette’s default state on an entry: recent pages, then the page’s actions.
Searching “image”
Searching “image”: fields on the page come first, with where each sits, then matching settings.
Searching “gallery”
Searching “gallery”: a field, its settings, assets with thumbnails, an entry and a delete action.
A result’s menu
A result’s menu, with the element’s own actions.
Searching “create”
Searching “create”: actions from across the control panel, each labeled with where it belongs.
Searching releases
Searching “spring”: a release, with its status, and its actions.
Publishing a release
Publishing a release: its drafts and whether each is ready. Drafts can be removed, and Publish is held while any is blocked.
Scheduling a release
Scheduling a release: a server-built form, shown in place of the results.
A confirmation
A destructive action confirming inside the palette.
Docs search
Searching the Craft CMS docs, with inline formatting such as code.
Keyboard shortcuts
Keyboard shortcuts.
Notifications
The user’s notifications, with Mark all as read in the view’s header.

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

📚 Storybook previews

@craftcms/ui — open Storybook

Changed components:

resources/js — open Storybook

No changed components detected in this Storybook.

@brandonkelly brandonkelly changed the title [6.x] Add the command palette [6.x] Command palette Oct 23, 2026

@riasvdv riasvdv left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d use our TypeRegistry or registries in general instead of events for registering pages, actions and search providers

brandonkelly and others added 18 commits October 25, 2026 11:09
A Cmd/Ctrl+K search over the control panel's pages, actions and content.
It searches the current page first: its fields, headings, tabs,
landmarks, Matrix blocks, sidebar panels and secondary nav. It also
searches the page's own actions, settings pages, element search results
and the Craft CMS glossary. Results can open views in the palette:
confirmations, server-built forms, keyboard shortcuts and glossary terms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings has a "Users" page too, and keying the results by label picked
whichever came last. That passed only while both had the same icon. Where
the solid icons aren't installed, as in CI, the Settings one keeps its light
icon, so "All users" was compared against the wrong parent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tries

Plugins added to the palette by listening for three resolving events. They
now register with singleton registries instead, as with field and widget
types:

- PageRegistry and ActionRegistry take a handle and a provider closure,
  called with the user, which returns the items to add.
- SearchProviderRegistry takes provider instances, or class names resolved
  from the container each time the palette asks, so a provider can depend
  on request-scoped services. Craft's own providers are its defaults.

All three can remove what's registered, Craft's search providers included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The palette reaches the slideouts module through element action menus, and
the slideouts reach PageScreen, which imported the palette. That cycle
deadlocked AdminTableData.browser.test.ts, whose slideouts mock awaits the
real module: CI's browser tests hung until the job timed out. PageScreen now
loads the palette as an async component.

So that Cmd+K works before that component arrives, the shortcut is installed
with the control panel rather than by the component, and a palette the
shortcut opened early finishes opening once it mounts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A result's menu came after the wider of its label and hint, centered on
both, so a long hint such as an asset's filename pushed it away from the
label. Rows are now two-line grids: the menu follows the label on the first
line, and the hint has the second to itself, where it can't push the menu.
The hint and icon sit beside the result's link or button rather than in it,
so the hint is read as a description, along with the category.

The menus' focus rings now sit just inside their edges, and take the
selected row's text color, as the accent color didn't stand out against it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Publish {release}" now opens a view with the release page's drafts
table, where drafts can be removed, rather than a confirmation. Publish
stays disabled while any draft is blocked, with a tooltip saying why.
The release page's Publish now and Schedule buttons take that reason
from the same place.

Server-built forms can now disable their submit button with a reason,
and tables within them reload the form when they change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tab and Shift+Tab now cycle between the search box, the focused result,
and its menu button, rather than moving on to other results, which the
arrow keys move between. Tabbing back to the search box focuses its input,
so typing goes into the search again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Esc Close", or "Esc Clear search" while there's a search.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Escape now does the same thing anywhere in the results: it clears the
search and returns to the search box, or closes the palette once there's
no search. An open result menu still closes first, and a view still goes
back to the results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A "Search Craft CMS documentation" action, listed from the start, opens a
view with its own search box. It searches the docs, Knowledge Base, and
glossary on craftcms.com, through a new command-palette/docs endpoint, and
opens results there in a new tab. Only this version of Craft's docs are
included. What's typed there is the only thing sent to craftcms.com.

Results show their titles' and summaries' inline formatting, such as code,
through new labelHtml and hintHtml item properties.

The glossary search provider and view are removed, as glossary terms now
come through the docs search.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Releases were listed among the palette's pages, capped at the 50 most
recent unpublished ones. A releases provider now searches them by name as
the user types, published ones included, unpublished ones first, with
their status and date as each result's hint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Craft.commandPalette.registerActions()` now returns `{unregister}` rather
than the unregister function itself, so it can return more later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It only lists the provider's results before everyone else's, which the new
name says, whether or not the results are about the page being viewed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The book icon was centered in its box, and is narrower than it, so it sat
a few pixels in from the "Results" heading. It's now flush with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The book icon kept a 24px-wide box, leaving a wide gap before each
result's title. It now takes only its own width, plus the small spacing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

This PR had conflicts with 6.x. Claude attempted an automatic resolution and pushed a merge commit. Please review the merge commit carefully before approving/merging.

Claude's response

I resolved the merge conflicts and committed the merge as f5ad7e3. Nothing was pushed, and I didn't run any builds or tests.

There was one conflicted file, resources/js/modules/ui/FieldNode.vue. On the field node's root element, each side had added a different attribute:

  • feature/command-palette added :data-icon="resolvedNode.props.icon || undefined".
  • 6.x added :aria-busy="generating || undefined", which marks the field as busy while a value is being generated.

The two attributes don't affect each other, so I kept both. The generating state that aria-busy relies on is defined in the merged file. No conflict was ambiguous, so I didn't leave any CONFLICT-REVIEW comments.

brandonkelly and others added 8 commits October 27, 2026 23:23
…nstalled

Uses the docs search API's product/version params instead of scopes and
client-side URL filtering. When Commerce is installed, its major version's
docs are searched in parallel and interleaved with Craft's results.

Amp-Thread-ID: https://ampcode.com/threads/T-d94ab28e-8c9d-40a0-a21a-96acf03a6584
Co-authored-by: Amp <amp@ampcode.com>
…r-icons

Match the header's search and notification triggers
@github-actions

Copy link
Copy Markdown

This PR had conflicts with 6.x. Claude attempted an automatic resolution and pushed a merge commit. Please review the merge commit carefully before approving/merging.

Claude's response

I resolved the merge and committed it as ce789d7. It hasn't been pushed.

The only conflicted file was CHANGELOG.md, which had two conflicts. In each one, both sides had added a new entry at the same spot in a list, so I kept both entries:

Neither conflict was ambiguous, so I didn't leave any CONFLICT-REVIEW comments, and no other files were changed.

@github-actions

Copy link
Copy Markdown

This PR had conflicts with 6.x. Claude attempted an automatic resolution and pushed a merge commit. Please review the merge commit carefully before approving/merging.

Claude's response

I've resolved the merge and committed it as 1015337. I didn't push. I didn't run any tests or checks, because the only file I edited was the changelog.

There was one conflict, in CHANGELOG.md. Both branches had added a changelog line at the same spot. This branch added #20106 (the header's notification button is now a plain icon button). 6.x added #20118 (plain links to control panel pages now open with Inertia instead of reloading the page). The two entries describe unrelated changes, so I kept both, with #20106 first and #20118 after it. Nothing was ambiguous, so I didn't leave any CONFLICT-REVIEW comments.

The other files from 6.x (CHANGELOG-WIP.md, resources/js/bootstrap/cp.ts, CpLink.vue, ProjectConfig.vue, and the new inertia-links.ts and its test) merged on their own without conflicts. I didn't change them.

brandonkelly and others added 9 commits October 28, 2026 04:43
The actions button followed the result under the pointer when it had
actions, so Tab from the search box could reach another result's actions
than the active option and the keyboard instructions said it would.

Amp-Thread-ID: https://ampcode.com/threads/T-c27fac24-ac8b-4256-be4b-7c1306ada663
Co-authored-by: Amp <amp@ampcode.com>
[6.x] Command palette: Budugbox with a listbox of results, and result actions after the listbox
Popovers set their invoker's start margin to the button group overlap,
which is unset outside a group, so it computed to 0 and replaced a flush
craft-button invoker's own margin. craft-button invokers now take the
overlap themselves, alongside their flush margin, and the popover only
sets it on other invokers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxhedKGW7eCLF4xLBVmh6o
Results with actions now show their button's icon until the actions menu
sits over them, rather than only when hovered or selected. The selected
row's button hovers in a tint of the row's own colors, so its icon stays
visible, and the Tab hint follows the Esc hint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxhedKGW7eCLF4xLBVmh6o
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxhedKGW7eCLF4xLBVmh6o
@brandonkelly
brandonkelly merged commit f9df20f into 6.x Oct 28, 2026
20 checks passed
@brandonkelly
brandonkelly deleted the feature/command-palette branch October 28, 2026 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants