Register executable commands discoverable through a built-in palette with keyboard shortcuts.
import { defineCommand } from '@vitejs/devtools-kit'
const clearCache = defineCommand({
id: 'my-plugin:clear-cache',
title: 'Clear Build Cache',
description: 'Remove all cached build artifacts',
icon: 'ph:trash-duotone',
category: 'tools',
handler: async () => {
await fs.rm('.cache', { recursive: true })
},
})
// Register in plugin setup
ctx.commands.register(clearCache)| Field | Type | Description |
|---|---|---|
id |
string |
Required. Unique namespaced ID (e.g. my-plugin:action) |
title |
string |
Required. Human-readable title shown in the palette |
description |
string |
Optional description text |
icon |
string |
Iconify icon string (e.g. ph:trash-duotone) |
category |
string |
Category for grouping |
showInPalette |
boolean | 'without-children' |
Whether to show in command palette (default: true). 'without-children' shows the command but doesn't flatten children into search — only accessible via drill-down. |
when |
string |
Conditional visibility expression — see When Clauses |
keybindings |
DevToolsCommandKeybinding[] |
Default keyboard shortcuts |
handler |
Function |
Server-side handler. Optional if the command is a group for children. |
children |
DevToolsServerCommandInput[] |
Static sub-commands (two levels max) |
register() returns a handle for live updates:
const handle = ctx.commands.register({
id: 'my-plugin:status',
title: 'Show Status',
handler: () => { /* ... */ },
})
// Update later
handle.update({ title: 'Show Status (3 items)' })
// Remove
handle.unregister()Two-level hierarchy. Selecting a parent in the palette drills down into its children.
ctx.commands.register({
id: 'git',
title: 'Git',
icon: 'ph:git-branch-duotone',
category: 'tools',
// No handler — group-only parent
children: [
{
id: 'git:commit',
title: 'Commit',
icon: 'ph:check-duotone',
keybindings: [{ key: 'Mod+Shift+G' }],
handler: async () => { /* ... */ },
},
{
id: 'git:push',
title: 'Push',
handler: async () => { /* ... */ },
},
],
})Each child must have a globally unique id. Use the pattern parentId:childAction (e.g. git:commit).
Sub-commands with keybindings can be executed directly via the shortcut without opening the palette.
Use Mod as a platform-aware modifier — maps to Cmd on macOS and Ctrl on other platforms.
| Key string | macOS | Windows/Linux |
|---|---|---|
Mod+K |
Cmd+K |
Ctrl+K |
Mod+Shift+P |
Cmd+Shift+P |
Ctrl+Shift+P |
Alt+N |
Option+N |
Alt+N |
ctx.commands.register(defineCommand({
id: 'my-plugin:toggle-overlay',
title: 'Toggle Overlay',
keybindings: [{ key: 'Mod+Shift+O' }],
handler: () => { /* ... */ },
}))Users can customize shortcuts in Settings > Keyboard Shortcuts. Overrides persist across sessions. Setting an empty array disables a shortcut.
The shortcut editor includes:
- Key capture — click the input and press any key combination
- Modifier toggles — toggle Cmd/Ctrl, Alt, Shift individually
- Conflict detection — warns on browser shortcut conflicts, duplicate bindings, and weak shortcuts (single key without modifiers)
KNOWN_BROWSER_SHORTCUTS is exported from @vitejs/devtools-kit and maps key combinations to human-readable descriptions.
Commands support a when expression for conditional visibility:
ctx.commands.register(defineCommand({
id: 'my-plugin:embedded-only',
title: 'Embedded-Only Action',
when: 'clientType == embedded',
handler: async () => { /* ... */ },
}))When set, the command only appears in the palette and is only triggerable via shortcuts when the expression evaluates to true.
See When Clauses for full syntax reference and context variables.
Client commands register in the webcomponent context and execute directly in the browser:
context.commands.register({
id: 'devtools:theme',
source: 'client',
title: 'Theme',
icon: 'ph:palette-duotone',
children: [
{
id: 'devtools:theme:light',
source: 'client',
title: 'Light',
action: () => setTheme('light'),
},
{
id: 'devtools:theme:dark',
source: 'client',
title: 'Dark',
action: () => setTheme('dark'),
},
],
})Client commands can return dynamic sub-items:
context.commands.register({
id: 'devtools:docs',
source: 'client',
title: 'Documentation',
action: async () => {
const docs = await fetchDocs()
return docs.map(doc => ({
id: `docs:${doc.slug}`,
source: 'client' as const,
title: doc.title,
action: () => window.open(doc.url, '_blank'),
}))
},
})Built-in palette toggled with Mod+K. Features:
- Fuzzy search across all registered commands (including sub-commands)
- Keyboard navigation — Arrow keys, Enter to select, Escape to close
- Drill-down — Commands with children show breadcrumb navigation
- Server command execution — via RPC with loading indicator
- Dynamic sub-menus — Client commands can return sub-items at runtime
/// <reference types="@vitejs/devtools-kit" />
import type { Plugin } from 'vite'
import { defineCommand } from '@vitejs/devtools-kit'
export default function myPlugin(): Plugin {
return {
name: 'my-plugin',
devtools: {
setup(ctx) {
// Simple command with keybinding
ctx.commands.register(defineCommand({
id: 'my-plugin:restart',
title: 'Restart Dev Server',
icon: 'ph:arrow-clockwise-duotone',
keybindings: [{ key: 'Mod+Shift+R' }],
handler: async () => {
await ctx.viteServer?.restart()
},
}))
// Command group with sub-commands
ctx.commands.register(defineCommand({
id: 'my-plugin:cache',
title: 'Cache',
icon: 'ph:database-duotone',
children: [
{
id: 'my-plugin:cache:clear',
title: 'Clear Cache',
handler: async () => { /* ... */ },
},
{
id: 'my-plugin:cache:inspect',
title: 'Inspect Cache',
handler: async () => { /* ... */ },
},
],
}))
},
},
}
}