The Command Palette was introduced in WordPress 6.3, along with an API that allows developers to extend its capabilities with additional commands.
This type of functionality is common in many other applications and programs. In WordPress, you press Cmd + K on Mac or Ctrl + K on Windows, and a modal window appears on the screen. From there, you simply type or search for commands.
All of the Command Palette functionality is available through the @wordpress/commands package.
If you want a good introduction to creating your own commands for the WordPress Command Palette, this post is a great place to start:
Last Friday I did a stream (In Spanish) to explore the Command Palette API that since WP 6.9 can be used in all admin pages. Before WP 6.9 the command palette commands were only available inside Post Editor and Site Editor pages.
During that stream I ran into a few challenges and realized there were several situations where I wasn’t sure how to handle registering commands outside the Block Editor scope.
The main issue was that some of the helper utilities for registering commands were only available as React hooks. Outside of Block Editor pages there’s no React runtime, and functions like registerPlugin can’t be used to execute React code (or hooks within it).
Another challenge was figuring out how to reliably detect the current context so that commands are registered conditionally, only when specific criteria are met.
After experimenting a bit more I came upon some examples to showcase the different contexts where a command for the the command palette can be registered. This guide walks through multiple ways to register commands, covering global usage, editor-only tools, conditional logic, and classic-page support — with real examples.
All examples in this article are available in the repo https://github.com/juanma-wp/command-palette-exploration
Table of Contents
- Register on All Admin Pages
- Register on All Admin Pages (with hooks)
- Register Commands Only in the Block Editor
- Register Commands Based on Editor Conditions
- Register Commands Only on Classic Admin Pages
- dispatch().registerCommand() vs useCommand()
- Conclusion
Register on All Admin Pages
You can register commands from anywhere in your plugin using dispatch().registerCommand(). These commands do not require React rendering and work across all admin pages.
Example: Open WordPress Developer Docs Anywhere
The following code register commands to quickly access some of the WordPress handbooks
import { dispatch } from "@wordpress/data";
import { store as commandsStore } from "@wordpress/commands";
const handbookLinks = [
{
name: "block-editor",
label: "Block Editor Handbook",
url: "https://developer.wordpress.org/block-editor/",
},
{
name: "plugin-dev",
label: "Plugin Developer Handbook",
url: "https://developer.wordpress.org/plugins/",
},
{
name: "theme-dev",
label: "Theme Developer Handbook",
url: "https://developer.wordpress.org/themes/",
},
{
name: "rest-api",
label: "REST API Handbook",
url: "https://developer.wordpress.org/rest-api/",
},
];
handbookLinks.forEach((link) => {
dispatch(commandsStore).registerCommand({
name: `myplugin/docs/${link.name}`,
label: link.label,
callback: () => window.open(link.url, "_blank"),
});
});Register on All Admin Pages (with hooks)
If your command needs React state, modals, or WordPress data stores (useSelect, useDispatch, etc.), render a global React app by creating a no-UI React component that runs the necessary internal WordPress hooks and mounting it manually with domReady() and createRoot() so it’s available across all admin screens.
Example: Show a Modal with Latest Posts (All Admin Screens)
This command opens a modal listing latest posts edited
import domReady from "@wordpress/dom-ready";
import { useCommand } from "@wordpress/commands";
import { useSelect } from "@wordpress/data";
import { Modal, Spinner, ExternalLink } from "@wordpress/components";
import { __ } from "@wordpress/i18n";
import { postList } from "@wordpress/icons";
import { store as coreStore } from "@wordpress/core-data";
import { useState } from "react";
const LatestPostsModal = ({ onClose }) => {
const { posts, isLoading } = useSelect((select) => {
const query = {
per_page: 5, // 🔢 Limit to 5 most recently modified
order: "desc",
orderby: "modified",
};
const posts = select(coreStore).getEntityRecords("postType", "post", query);
const isLoading = !Array.isArray(posts);
return { posts, isLoading };
}, []);
return (
<Modal
title={__("Recently Edited Posts", "myplugin")}
onRequestClose={onClose}
focusOnMount
>
{isLoading ? (
<Spinner />
) : (
<ul>
{posts.map((post) => (
<li key={post.id} style={{ marginBottom: "1rem" }}>
<strong>
{post.title.rendered || __("Untitled", "myplugin")}
</strong>
<br />
{__("Created", "myplugin")}:{" "}
{new Date(post.date).toLocaleString()}
<br />
{__("Updated", "myplugin")}:{" "}
{new Date(post.modified).toLocaleString()}
<br />
<ExternalLink
href={post.link}
target="_blank"
rel="noopener noreferrer"
>
{__("View Post", "myplugin")}
</ExternalLink>{" "}
|{" "}
<a
href={`post.php?post=${post.id}&action=edit`}
target="_blank"
rel="noopener noreferrer"
>
{__("Edit", "myplugin")}
</a>
</li>
))}
</ul>
)}
</Modal>
);
};
const LatestPostsCommand = () => {
const [isOpen, setIsOpen] = useState(false);
const { postsCount } = useSelect((select) => {
const query = {
per_page: 5,
order: "desc",
orderby: "modified",
};
const posts = select(coreStore).getEntityRecords("postType", "post", query);
return {
postsCount: Array.isArray(posts) ? posts.length : 0
};
}, []);
useCommand({
name: "myplugin/show-latest-posts",
label: postsCount > 0
? __(`Show Latest Posts (${postsCount})`, "myplugin")
: __("Show Latest Posts", "myplugin"),
icon: postList,
callback: () => setIsOpen(true),
});
return isOpen ? <LatestPostsModal onClose={() => setIsOpen(false)} /> : null;
};
domReady(() => {
const container = document.createElement("div");
container.id = "myplugin-global-hook-commands";
document.body.appendChild(container);
createRoot(container).render(<LatestPostsCommand />);
});
Register Commands Only in the Block Editor
When your command is meant only for the post/page editing experience, use registerPlugin() so it’s scoped to the Block Editor UI.
Example: Copy Post Content to Clipboard
import { registerPlugin } from "@wordpress/plugins";
import { useCommand } from "@wordpress/commands";
import { copy } from "@wordpress/icons";
import { useSelect, useDispatch, select } from "@wordpress/data";
import { store as noticesStore } from "@wordpress/notices";
import { store as editorStore } from "@wordpress/editor";
import { __ } from "@wordpress/i18n";
export default function CopyPostContent () {
const { createNotice } = useDispatch(noticesStore);
const hasContent = useSelect((select) => {
const content = select(editorStore).getEditedPostContent();
return !!content && content.trim().length > 0;
}, []);
useCommand({
name: "myplugin/copy-post-content",
label: hasContent
? __("Copy Post Content to Clipboard", "myplugin")
: __("Copy Post Content (No content)", "myplugin"),
icon: copy,
context: "entity-edit",
disabled: !hasContent,
callback: ({ close }) => {
const content = select(editorStore).getEditedPostContent();
if (!content) {
createNotice(
"error",
__("No post content to copy.", "myplugin"),
{ type: "snackbar" }
);
close();
return;
}
navigator.clipboard.writeText(content).then(() => {
createNotice(
"success",
__("Post content copied to clipboard!", "myplugin"),
{ type: "snackbar" }
);
});
close();
},
});
return null;
};
registerPlugin("myplugin-copy-post-content", {
render: CopyPostContent,
});The @wordpress/plugins package is specifically designed for the Block Editor context and enable the use of Slot Fills like like PluginSidebar or PluginMoreMenuItem
Register Commands Based on Editor Conditions
Sometimes, you want a command only when certain conditions are met:
- You’re in the post editor
- The user can edit the post
Example: Show Block Usage (only if user can edit post)
This command will display a modal listing the block used in the current post. This command will only be available in the Post Editor and only if the user can edit posts.
import { registerPlugin } from "@wordpress/plugins";
import { useState, useMemo } from "@wordpress/element";
import { useCommandLoader } from "@wordpress/commands";
import { Modal } from "@wordpress/components";
import { useSelect } from "@wordpress/data";
import { store as blockEditorStore } from "@wordpress/block-editor";
import { store as editorStore } from "@wordpress/editor";
import { store as coreStore } from "@wordpress/core-data";
import { listView } from "@wordpress/icons";
import { __ } from "@wordpress/i18n";
/**
* Custom hook to count block usage in the current post/page.
* Recursively walks through all blocks including nested ones.
*
* @return {Object} Object with block names as keys and their counts as values
*/
function useBlockCounts() {
const blocks = useSelect((select) => select(blockEditorStore).getBlocks());
return useMemo(() => {
const counts = {};
const walkBlocks = (blocks) => {
blocks.forEach((block) => {
counts[block.name] = (counts[block.name] || 0) + 1;
if (block.innerBlocks?.length) {
walkBlocks(block.innerBlocks);
}
});
};
walkBlocks(blocks);
return counts;
}, [blocks]);
}
/**
* Custom hook to check if the current post type is viewable on the frontend.
*
* @return {boolean} True if the post type is viewable, false otherwise
*/
function useIsViewablePostType() {
return useSelect((select) => {
const { getCurrentPostType } = select(editorStore);
const { getPostType } = select(coreStore);
return getPostType(getCurrentPostType())?.viewable ?? false;
}, []);
}
/**
* Modal component to display the block usage.
*
* @param {Object} props - The component props
* @param {Function} props.onClose - The function to close the modal
* @returns {JSX.Element} The modal component
*/
const BlockUsageModal = ({ onClose }) => {
const blockCounts = useBlockCounts();
const hasBlocks = Object.keys(blockCounts).length > 0;
return (
<Modal title={__("Block Usage", "myplugin")} onRequestClose={onClose}>
{hasBlocks ? (
<ul>
{Object.entries(blockCounts).map(([name, count]) => (
<li key={name}>
<strong>{name}</strong>: {count}
</li>
))}
</ul>
) : (
<p>{__("No blocks found in this content.", "myplugin")}</p>
)}
</Modal>
);
};
function useBlockUsageCommands(onOpen) {
const isViewable = useIsViewablePostType();
const commands = useMemo(() => {
if (!isViewable) return [];
return [
{
name: "myplugin/show-block-usage",
label: __("Show Block Usage", "myplugin"),
icon: listView,
context: "entity-edit",
callback: ({ close }) => {
onOpen();
close();
},
},
];
}, [isViewable, onOpen]);
return { commands, isLoading: false };
}
function BlockUsageCommand() {
const [isOpen, setIsOpen] = useState(false);
useCommandLoader({
name: "myplugin/block-usage-loader",
hook: () => useBlockUsageCommands(() => setIsOpen(true)),
});
return isOpen ? <BlockUsageModal onClose={() => setIsOpen(false)} /> : null;
}
registerPlugin("myplugin-copy-post-content", {
render: BlockUsageCommand,
});
This command dynamically registers only in the right conditions and uses hooks to manage modal state.
The viewable property in the snippet above code determines whether the current post type has a public-facing edit screen in the WordPress admin interface. When postTypeObject?.viewable === true, it means we’re editing a post type that supports the traditional post editor experience (like posts or pages), rather than an internal post type (like templates or template parts
Register Commands Only on Classic Admin Pages
Want a command that only shows on non-Gutenberg screens (e.g., plugin settings, classic UI)?
Use a custom check like !isGutenbergContext():
Example: Activate/Deactivate Plugin (only outside Gutenberg)
import { useEffect, useState } from "@wordpress/element";
import { useSelect } from "@wordpress/data";
import { useCommandLoader } from "@wordpress/commands";
import { store as editorStore } from "@wordpress/editor";
import { plugins } from "@wordpress/icons";
import apiFetch from "@wordpress/api-fetch";
import to from "await-to-js";
/**
* Fetches all installed WordPress plugins via the REST API.
*
* @return {Promise<Array>} Array of plugin objects with status, name, and plugin file info
*/
async function fetchPlugins() {
return apiFetch({ path: "/wp/v2/plugins" });
}
/**
* Toggles a plugin's activation status via the WordPress REST API.
*
* @param {string} action - The action to perform ("activate" or "deactivate")
* @param {string} pluginFile - The plugin file identifier (e.g., "plugin-name/plugin-name.php")
*/
async function togglePlugin(action, pluginFile) {
try {
const status = action === "activate" ? "active" : "inactive";
await apiFetch({
path: `/wp/v2/plugins/${encodeURIComponent(pluginFile)}`,
method: "POST",
data: { status },
});
// Redirect to plugins page after successful toggle
window.location.href = "plugins.php";
} catch (error) {
console.error(`Failed to ${action} plugin:`, error);
window.location.href = "plugins.php";
}
}
/**
* Hook that returns plugin commands for useCommandLoader
*/
function usePluginCommands() {
const [allPlugins, setAllPlugins] = useState([]);
const [isLoading, setIsLoading] = useState(true);
// Check if we're in the block editor by checking for current post
const isBlockEditor = useSelect((select) => {
try {
const postId = select(editorStore)?.getCurrentPostId?.();
return postId !== undefined && postId !== null;
} catch {
return false;
}
}, []);
useEffect(() => {
// Only fetch plugins if we're NOT in the block editor
if (!isBlockEditor) {
(async () => {
const [error, plugins] = await to(fetchPlugins());
if (!error && plugins) {
setAllPlugins(plugins);
}
setIsLoading(false);
})();
} else {
setIsLoading(false);
}
}, [isBlockEditor]);
const commands = [];
// Only register commands if NOT in block editor
if (!isBlockEditor && !isLoading) {
commands.push({
name: "myplugin/open-settings",
label: "Open Plugin Settings",
callback: () => (window.location.href = "admin.php?page=myplugin-settings"),
});
allPlugins.forEach((plugin) => {
const isActive = plugin.status === "active";
const pluginSlug = plugin.plugin;
const pluginName = plugin.name;
if (!isActive) {
commands.push({
name: `myplugin/activate-${pluginSlug}`,
label: `Activate ${pluginName}`,
icon: plugins,
callback: () => togglePlugin("activate", pluginSlug),
});
} else {
commands.push({
name: `myplugin/deactivate-${pluginSlug}`,
label: `Deactivate ${pluginName}`,
icon: plugins,
callback: () => togglePlugin("deactivate", pluginSlug),
});
}
});
}
return { commands, isLoading };
}
export { usePluginCommands };
The getCurrentPostId() function determines which Gutenberg editor context you’re currently in by checking for the current post id.
// 3- Register plugin actions command with custom hydration
const PluginActionsCommand = () => {
useCommandLoader({
name: "myplugin/plugin-actions-loader",
hook: usePluginCommands,
});
return null;
};
domReady(() => {
const container = document.createElement("div");
container.id = "myplugin-plugin-actions-commands";
document.body.appendChild(container);
createRoot(container).render(<PluginActionsCommand />);
});dispatch().registerCommand() vs useCommand()
There are two main ways to register commands in WordPress:
| Method | dispatch().registerCommand() | useCommand() |
|---|---|---|
| Where it runs | Anywhere in JS | Inside React component |
| Uses React hooks | No | Yes |
| UI elements (modals, notices) | No | Yes |
| Reactive to state | No | Yes |
| Dependencies | Only @wordpress/data | Requires @wordpress/element + render |
| Use Case | Utility/Global commands | UI or editor-bound commands |
dispatch().registerCommand() Example
import { dispatch } from "@wordpress/data";
import { store as commandsStore } from "@wordpress/commands";
dispatch(commandsStore).registerCommand({
name: "myplugin/reload",
label: "Reload Page",
callback: () => {
location.reload();
},
});useCommand() Example
useCommand({
name: "myplugin/show-modal",
label: "Open Modal",
callback: () => setIsOpen(true),
});✅ Use dispatch().registerCommand() when hooks aren’t needed
✅ Use useCommand() for anything interactive or dynamic
As part of the exploration I did after my Stream last Friday about the Commands Palette API I have prototyped a command to show/hide the new Notes Sidebar Panel and opened an issue to start the discussion about including such a command in Gutenberg.
Conclusion
The Command Palette provides a fast and flexible way to add powerful actions to the WordPress admin. We can register commands globally, limit them to the editor, or show them only when certain conditions are met. Depending on what we need, we can use either dispatch() for simple actions or useCommand() for interactive features like modals and notices.
There’s a lot that can be done with the Command Palette. After this exploration there are a lot of commands I’d like to implement.
One idea would be, having a command that allows to search in any on the handbooks any text, for example by launching a special command palette with “Command + May + K”. Shall I try it?
After the learnings of my first stream about the Commands Palette (in Spanish) I did another stream in English exploring the creation of custom commands for the Commands Palette

Leave a Reply