Skip to content

Dock System

Dock entries are how users open your DevTools integration — clickable items in the dock, similar to the macOS Dock.

Entry types

Kit supports five dock entry types:

TypeDescriptionUse Case
iframeDisplays your UI in an iframe panelFull-featured UIs, dashboards, data visualization
actionButton that triggers client-side scriptsInspectors, toggles, one-time actions
custom-renderRenders directly in the user's app DOMWhen you need direct DOM access or framework integration
launcherActionable setup card shown in panelRun one-time setup tasks before showing other tools
json-renderRenders UI from a JSON spec — no client code neededData panels, config viewers, simple interactive tools

Iframe panels

The default choice — host your UI in an iframe. The frame stays isolated from the user's app and works with any framework.

Basic example

ts
ctx.docks.register({
  id: 'my-plugin',
  title: 'My Plugin',
  icon: 'https://example.com/logo.svg',
  type: 'iframe',
  url: 'https://example.com/devtools',
})

Hosting your own UI

For most use cases, you build and host your own UI. DevTools serves the static files:

ts
import { fileURLToPath } from 'node:url'

// Path to your built SPA
const clientDist = fileURLToPath(new URL('../dist/client', import.meta.url))

// Host the static files
ctx.views.hostStatic('/__my-plugin/', clientDist)

// Register the dock entry
ctx.docks.register({
  id: 'my-plugin',
  title: 'My Plugin',
  icon: 'ph:puzzle-piece-duotone',
  type: 'iframe',
  url: '/__my-plugin/',
})

DevTools serves the files via dev-server middleware and copies them into the build output for production.

Dock entry options

ts
interface DockEntry {
  /** Unique identifier for this entry */
  id: string
  /** Display title shown in the dock */
  title: string
  /** Icon URL, data URI, or Iconify icon name (e.g., 'ph:house-duotone') */
  icon: string | { light: string, dark: string }
  /** Entry type */
  type: 'iframe' | 'action' | 'custom-render' | 'launcher' | 'json-render'
  /** URL to load in the iframe (for type: 'iframe') */
  url?: string
  /** Action configuration (for type: 'action') */
  action?: { importFrom: string, importName: string }
  /** Renderer configuration (for type: 'custom-render') */
  renderer?: { importFrom: string, importName: string }
  /** Launcher configuration (for type: 'launcher') */
  launcher?: {
    title: string
    onLaunch: () => Promise<void>
    description?: string
    buttonStart?: string
    buttonLoading?: string
  }
  /** JsonRenderer handle created by ctx.createJsonRenderer() (for type: 'json-render') */
  ui?: JsonRenderer
}

Icons

Icons accept a URL, a data URI, or an Iconify name. The ph: (Phosphor) set pairs well with DevTools UIs.

ts
// URL to an image
icon: 'https://example.com/logo.svg'

// Data URI
icon: 'data:image/svg+xml,...'

// Iconify icon name
icon: 'ph:chart-bar-duotone' // Phosphor Icons
icon: 'carbon:analytics' // Carbon Icons
icon: 'mdi:view-dashboard' // Material Design Icons

// Light/dark variants
icon: {
  light: 'https://example.com/logo-light.svg'
  dark: 'https://example.com/logo-dark.svg'
}

The File Explorer example is a complete iframe-dock plugin with RPC and static-build support.

Remote-hosted UIs

To skip bundling a dist with your plugin, an iframe dock can point at a hosted website that connects back to the local dev server over WebSocket. See Remote Client.

Action buttons

Action buttons run a client-side script when clicked. They suit:

  • Temporary inspector tools (DOM inspector, component picker).
  • Feature toggles.
  • One-shot actions where a button is enough.

Registration

ts
ctx.docks.register({
  id: 'my-inspector',
  title: 'Inspector',
  icon: 'ph:cursor-duotone',
  type: 'action',
  action: {
    importFrom: 'my-plugin/devtools-action',
    importName: 'default',
  },
})

Client script

The action script runs in the user's browser:

ts
// src/devtools-action.ts
import type { DockClientScriptContext } from '@vitejs/devtools-kit/client'

export default function setupAction(ctx: DockClientScriptContext) {
  let isActive = false
  let overlay: HTMLElement | null = null

  ctx.current.events.on('entry:activated', () => {
    isActive = true
    console.log('Inspector activated')

    // Create an overlay
    overlay = document.createElement('div')
    overlay.style.cssText = `
      position: fixed;
      inset: 0;
      cursor: crosshair;
      z-index: 99999;
    `

    overlay.addEventListener('click', (e) => {
      const target = document.elementFromPoint(e.clientX, e.clientY)
      console.log('Selected element:', target)
    })

    document.body.appendChild(overlay)
  })

  ctx.current.events.on('entry:deactivated', () => {
    isActive = false
    console.log('Inspector deactivated')

    // Cleanup
    overlay?.remove()
    overlay = null
  })
}

Package export

Export the action script from your package:

json
{
  "name": "my-plugin",
  "exports": {
    ".": "./dist/index.mjs",
    "./devtools-action": "./dist/devtools-action.mjs"
  }
}

Available events

EventDescription
entry:activatedFires when the user activates this dock entry
entry:deactivatedFires when another entry is selected or the dock is closed

For a real-world action dock, see the A11y Checker example — it runs axe-core audits and reports violations as logs.

Custom renderers

Custom renderers paint directly into the DevTools panel DOM. Use them when you want direct DOM access, want to mount a framework app into the panel, or want to skip iframe isolation.

Registration

ts
ctx.docks.register({
  id: 'my-custom-view',
  title: 'Custom View',
  icon: 'ph:code-duotone',
  type: 'custom-render',
  renderer: {
    importFrom: 'my-plugin/devtools-renderer',
    importName: 'default',
  },
})

Renderer script

ts
// src/devtools-renderer.ts
import type { DockClientScriptContext } from '@vitejs/devtools-kit/client'

export default function setupRenderer(ctx: DockClientScriptContext) {
  ctx.current.events.on('dom:panel:mounted', (panel) => {
    // `panel` is a DOM element you can render into

    // Option 1: Vanilla JS
    panel.innerHTML = `
      <div style="padding: 16px;">
        <h2>My Custom View</h2>
        <button id="my-btn">Click me</button>
      </div>
    `
    panel.querySelector('#my-btn')?.addEventListener('click', () => {
      console.log('Button clicked!')
    })

    // Option 2: Mount a Vue app
    // import { createApp } from 'vue'
    // import App from './App.vue'
    // createApp(App).mount(panel)

    // Option 3: Mount a React app
    // import { createRoot } from 'react-dom/client'
    // import App from './App'
    // createRoot(panel).render(<App />)
  })

  ctx.current.events.on('entry:deactivated', () => {
    // Optional cleanup
  })
}

Available events

EventPayloadDescription
dom:panel:mountedHTMLElementPanel DOM is ready for rendering
entry:activatedEntry was activated
entry:deactivatedEntry was deactivated

The panel DOM is preserved across dock-entry switches, so your UI persists and the one-time setup belongs in dom:panel:mounted.

Launcher entries

Launchers render a dedicated setup panel and run a server-side launch task. They suit integrations that need an explicit initialization step — starting a terminal task, generating artifacts, and so on.

ts
ctx.docks.register({
  id: 'my-launcher',
  title: 'My Setup',
  icon: 'ph:rocket-launch-duotone',
  type: 'launcher',
  launcher: {
    title: 'Initialize Integration',
    description: 'Run initial setup before opening tools',
    onLaunch: async () => {
      // perform setup work here
    },
  },
})

JSON render panels

JSON render panels describe a UI as a JSON spec on the server — the client renders it from a built-in component library. This is the shortest path to a DevTools panel: server-side TypeScript only.

Create a renderer handle with ctx.createJsonRenderer() and pass it as ui when registering a json-render dock entry:

ts
const ui = ctx.createJsonRenderer({
  root: 'root',
  elements: {
    root: {
      type: 'Stack',
      props: { direction: 'vertical', gap: 12 },
      children: ['heading', 'info'],
    },
    heading: {
      type: 'Text',
      props: { content: 'Hello from JSON!', variant: 'heading' },
    },
    info: {
      type: 'KeyValueTable',
      props: {
        entries: [
          { key: 'Version', value: '1.0.0' },
          { key: 'Status', value: 'Running' },
        ],
      },
    },
  },
})

ctx.docks.register({
  id: 'my-panel',
  title: 'My Panel',
  icon: 'ph:chart-bar-duotone',
  type: 'json-render',
  ui,
})

See JSON Render for the full component reference, dynamic updates, actions, state bindings, and examples.

Common options

Every dock type accepts these base fields:

FieldTypeDescription
idstringUnique, namespaced.
titlestringLabel shown in the dock.
iconstring | { light, dark }Iconify name, URL, data URI, or light/dark pair.
category'app' | 'framework' | 'web' | 'advanced' | 'default'Grouping in the dock panel. Defaults to 'default'.
defaultOrdernumberHigher numbers appear first. Default 0.
whenstringVisibility expression — see When Clauses.
badgestringShort text badge (e.g. unread count).

Update

register() returns a handle with an update(patch) method:

ts
const handle = ctx.docks.register({ /* ... */ })

// Live update (e.g. refresh the badge)
handle.update({ badge: '3' })

Communication with the server

Action scripts and custom renderers talk to the server through RPC:

ts
import type { DockClientScriptContext } from '@vitejs/devtools-kit/client'

export default function setup(ctx: DockClientScriptContext) {
  ctx.current.events.on('entry:activated', async () => {
    // Call a server function
    const data = await ctx.rpc.call('my-plugin:get-data')
    console.log('Data from server:', data)
  })
}

Or use getDevToolsRpcClient() in iframe pages:

ts
import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'

const rpc = await getDevToolsRpcClient()
const data = await rpc.call('my-plugin:get-data')

See RPC for complete documentation on server-client communication.

Released under the MIT License.