Vite DevTools Kit
Build custom developer tools that integrate with Vite DevTools using @vitejs/devtools-kit .
Core Concepts
A DevTools plugin extends a Vite plugin with a devtools.setup(ctx) hook. The context provides:
Property Purpose
ctx.docks
Register dock entries (iframe, action, custom-render, launcher)
ctx.views
Host static files for UI
ctx.rpc
Register RPC functions, broadcast to clients
ctx.rpc.sharedState
Synchronized server-client state
ctx.logs
Emit structured log entries and toast notifications
ctx.viteConfig
Resolved Vite configuration
ctx.viteServer
Dev server instance (dev mode only)
ctx.mode
'dev' or 'build'
Quick Start: Minimal Plugin
/// <reference types="@vitejs/devtools-kit" /> import type { Plugin } from 'vite'
export default function myPlugin(): Plugin { return { name: 'my-plugin', devtools: { setup(ctx) { ctx.docks.register({ id: 'my-plugin', title: 'My Plugin', icon: 'ph:puzzle-piece-duotone', type: 'iframe', url: 'https://example.com/devtools', }) }, }, } }
Quick Start: Full Integration
/// <reference types="@vitejs/devtools-kit" /> import type { Plugin } from 'vite' import { fileURLToPath } from 'node:url' import { defineRpcFunction } from '@vitejs/devtools-kit'
export default function myAnalyzer(): Plugin { const data = new Map<string, { size: number }>()
return { name: 'my-analyzer',
// Collect data in Vite hooks
transform(code, id) {
data.set(id, { size: code.length })
},
devtools: {
setup(ctx) {
// 1. Host static UI
const clientPath = fileURLToPath(
new URL('../dist/client', import.meta.url)
)
ctx.views.hostStatic('/.my-analyzer/', clientPath)
// 2. Register dock entry
ctx.docks.register({
id: 'my-analyzer',
title: 'Analyzer',
icon: 'ph:chart-bar-duotone',
type: 'iframe',
url: '/.my-analyzer/',
})
// 3. Register RPC function
ctx.rpc.register(
defineRpcFunction({
name: 'my-analyzer:get-data',
type: 'query',
setup: () => ({
handler: async () => Array.from(data.entries()),
}),
})
)
},
},
} }
Namespacing Convention
CRITICAL: Always prefix RPC functions, shared state keys, and dock IDs with your plugin name:
// Good - namespaced 'my-plugin:get-modules' 'my-plugin:state'
// Bad - may conflict 'get-modules' 'state'
Dock Entry Types
Type Use Case
iframe
Full UI panels, dashboards (most common)
action
Buttons that trigger client-side scripts (inspectors, toggles)
custom-render
Direct DOM access in panel (framework mounting)
launcher
Actionable setup cards for initialization tasks
Iframe Entry
ctx.docks.register({ id: 'my-plugin', title: 'My Plugin', icon: 'ph:house-duotone', type: 'iframe', url: '/.my-plugin/', })
Action Entry
ctx.docks.register({ id: 'my-inspector', title: 'Inspector', icon: 'ph:cursor-duotone', type: 'action', action: { importFrom: 'my-plugin/devtools-action', importName: 'default', }, })
Custom Render Entry
ctx.docks.register({ id: 'my-custom', title: 'Custom View', icon: 'ph:code-duotone', type: 'custom-render', renderer: { importFrom: 'my-plugin/devtools-renderer', importName: 'default', }, })
Launcher Entry
const entry = ctx.docks.register({ id: 'my-setup', title: 'My Setup', icon: 'ph:rocket-launch-duotone', type: 'launcher', launcher: { title: 'Initialize My Plugin', description: 'Run initial setup before using the plugin', buttonStart: 'Start Setup', buttonLoading: 'Setting up...', onLaunch: async () => { // Run initialization logic }, }, })
Logs & Notifications
Plugins can emit structured log entries from both server and client contexts. Logs appear in the built-in Logs panel and can optionally show as toast notifications.
Fire-and-Forget
// No await needed context.logs.add({ message: 'Plugin initialized', level: 'info', })
With Handle
const handle = await context.logs.add({ id: 'my-build', message: 'Building...', level: 'info', status: 'loading', })
// Update later await handle.update({ message: 'Build complete', level: 'success', status: 'idle', })
// Or dismiss await handle.dismiss()
Key Fields
Field Type Description
message
string
Short title (required)
level
'info' | 'warn' | 'error' | 'success' | 'debug'
Severity (required)
description
string
Detailed description
notify
boolean
Show as toast notification
filePosition
{ file, line?, column? }
Source file location (clickable)
elementPosition
{ selector?, boundingBox?, description? }
DOM element position
id
string
Explicit id for deduplication
status
'loading' | 'idle'
Shows spinner when loading
category
string
Grouping (e.g., 'a11y' , 'lint' )
labels
string[]
Tags for filtering
autoDismiss
number
Toast auto-dismiss time in ms (default: 5000)
autoDelete
number
Auto-delete time in ms
The from field is automatically set to 'server' or 'browser' .
Deduplication
Re-adding with the same id updates the existing entry instead of creating a duplicate:
context.logs.add({ id: 'my-scan', message: 'Scanning...', level: 'info', status: 'loading' }) context.logs.add({ id: 'my-scan', message: 'Scan complete', level: 'success', status: 'idle' })
RPC Functions
Server-Side Definition
import { defineRpcFunction } from '@vitejs/devtools-kit'
const getModules = defineRpcFunction({ name: 'my-plugin:get-modules', type: 'query', // 'query' | 'action' | 'static' setup: ctx => ({ handler: async (filter?: string) => { // ctx has full DevToolsNodeContext return modules.filter(m => !filter || m.includes(filter)) }, }), })
// Register in setup ctx.rpc.register(getModules)
Client-Side Call (iframe)
import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
const rpc = await getDevToolsRpcClient() const modules = await rpc.call('my-plugin:get-modules', 'src/')
Client-Side Call (action/renderer script)
import type { DevToolsClientScriptContext } from '@vitejs/devtools-kit/client'
export default function setup(ctx: DevToolsClientScriptContext) { ctx.current.events.on('entry:activated', async () => { const data = await ctx.current.rpc.call('my-plugin:get-data') }) }
Broadcasting to Clients
// Server broadcasts to all clients ctx.rpc.broadcast({ method: 'my-plugin:on-update', args: [{ changedFile: '/src/main.ts' }], })
Type Safety
Extend the DevTools Kit interfaces for full type checking:
// src/types.ts import '@vitejs/devtools-kit'
declare module '@vitejs/devtools-kit' { interface DevToolsRpcServerFunctions { 'my-plugin:get-modules': (filter?: string) => Promise<Module[]> }
interface DevToolsRpcClientFunctions { 'my-plugin:on-update': (data: { changedFile: string }) => void }
interface DevToolsRpcSharedStates { 'my-plugin:state': MyPluginState } }
Shared State
Server-Side
const state = await ctx.rpc.sharedState.get('my-plugin:state', { initialValue: { count: 0, items: [] }, })
// Read console.log(state.value())
// Mutate (auto-syncs to clients) state.mutate((draft) => { draft.count += 1 draft.items.push('new item') })
Client-Side
const client = await getDevToolsRpcClient() const state = await client.rpc.sharedState.get('my-plugin:state')
// Read console.log(state.value())
// Subscribe to changes state.on('updated', (newState) => { console.log('State updated:', newState) })
Client Scripts
For action buttons and custom renderers:
// src/devtools-action.ts import type { DevToolsClientScriptContext } from '@vitejs/devtools-kit/client'
export default function setup(ctx: DevToolsClientScriptContext) { ctx.current.events.on('entry:activated', () => { console.log('Action activated') // Your inspector/tool logic here })
ctx.current.events.on('entry:deactivated', () => { console.log('Action deactivated') // Cleanup }) }
Export from package.json:
{ "exports": { ".": "./dist/index.mjs", "./devtools-action": "./dist/devtools-action.mjs" } }
Debugging with Self-Inspect
Use @vitejs/devtools-self-inspect to debug your DevTools plugin. It shows registered RPC functions, dock entries, client scripts, and plugins in a meta-introspection UI at /.devtools-self-inspect/ .
import DevTools from '@vitejs/devtools' import DevToolsSelfInspect from '@vitejs/devtools-self-inspect'
export default defineConfig({ plugins: [ DevTools(), DevToolsSelfInspect(), ], })
Best Practices
-
Always namespace - Prefix all identifiers with your plugin name
-
Use type augmentation - Extend DevToolsRpcServerFunctions for type-safe RPC
-
Keep state serializable - No functions or circular references in shared state
-
Batch mutations - Use single mutate() call for multiple changes
-
Host static files - Use ctx.views.hostStatic() for your UI assets
-
Use Iconify icons - Prefer ph:* (Phosphor) icons: icon: 'ph:chart-bar-duotone'
-
Deduplicate logs - Use explicit id for logs representing ongoing operations
-
Use Self-Inspect - Add @vitejs/devtools-self-inspect during development to debug your plugin
Example Plugins
Real-world example plugins in the repo — reference their code structure and patterns when building new integrations:
-
A11y Checker (examples/plugin-a11y-checker ) — Action dock entry, client-side axe-core audits, logs with severity levels and element positions, log handle updates
-
File Explorer (examples/plugin-file-explorer ) — Iframe dock entry, RPC functions (static/query/action), hosted UI panel, RPC dump for static builds, backend mode detection
Further Reading
-
RPC Patterns - Advanced RPC patterns and type utilities
-
Dock Entry Types - Detailed dock configuration options
-
Shared State Patterns - Framework integration examples
-
Project Structure - Recommended file organization
-
Logs Patterns - Log entries, toast notifications, and handle patterns