Skip to content

Messages & Notifications

The Messages system lets plugins emit structured message entries from both the server (Node.js) and client (browser) contexts. Entries appear in the Messages panel in the DevTools dock and can optionally surface as toast notifications. For coded errors and warnings with stable codes and docs URLs, use Structured Diagnostics (ctx.diagnostics) instead.

Use cases

  • Accessibility audits — run a11y checks on the client and report warnings with element positions.
  • Runtime errors — capture and display errors with stack traces.
  • Linting & testing — run ESLint or test runners alongside the dev server and surface results with file positions.
  • Notifications — short-lived messages like "URL copied" that auto-dismiss.

Message entry fields

FieldTypeRequiredDescription
messagestringYesShort title or summary
level'info' | 'warn' | 'error' | 'success' | 'debug'YesSeverity level, determines color and icon
descriptionstringNoDetailed description or explanation
stacktracestringNoStack trace string
filePosition{ file, line?, column? }NoSource file location (clickable in the panel)
elementPosition{ selector?, boundingBox?, description? }NoDOM element position info
notifybooleanNoShow as a toast notification
categorystringNoGrouping category (e.g., 'a11y', 'lint')
labelsstring[]NoTags for filtering
autoDismissnumberNoTime in ms to auto-dismiss the toast (default: 5000)
autoDeletenumberNoTime in ms to auto-delete the message entry
status'loading' | 'idle'NoStatus indicator (shows spinner when 'loading')
idstringNoExplicit id for deduplication — re-adding with the same id updates the existing entry

The from field is set to 'server' or 'browser' automatically, based on where the message was emitted.

Usage

Server and client share the same context.messages API. Methods return Promises; for fire-and-forget usage, skip the await.

Fire-and-forget

ts
// No await needed — just emit the message
context.messages.add({
  message: 'Plugin initialized',
  level: 'info',
})

With handle

await the add() call for a DevToolsMessageHandle you can update later:

ts
// Await to get a handle for later updates
const handle = await context.messages.add({
  id: 'my-build',
  message: 'Building...',
  level: 'info',
  status: 'loading',
})

// Later, update via the handle
await handle.update({
  message: 'Build complete',
  level: 'success',
  status: 'idle',
})

// Or dismiss it
await handle.dismiss()

Server-side example

ts
export function myPlugin() {
  return {
    name: 'my-plugin',
    devtools: {
      setup(context) {
        // Fire-and-forget
        context.messages.add({
          message: 'Plugin initialized',
          level: 'info',
        })
      },
    },
  }
}

Client-side example

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

export default async function (context: DockClientScriptContext) {
  // Await to get the handle
  const message = await context.messages.add({
    message: 'Running audit...',
    level: 'info',
    status: 'loading',
    notify: true,
  })

  // ... do work ...

  // Update via handle — can also be fire-and-forget
  message.update({
    message: 'Audit complete — 3 issues found',
    level: 'warn',
    status: 'idle',
  })
}

Message handle

context.messages.add() returns a Promise<DevToolsMessageHandle> with:

Property/MethodDescription
handle.idThe message entry id
handle.entryThe current DevToolsMessageEntry data
handle.update(patch)Partially update the message entry (returns Promise)
handle.dismiss()Remove the message entry (returns Promise)

Both handle.update() and handle.dismiss() return Promises; the await is optional.

Deduplication

Calling add() with an explicit id that already exists updates the existing entry rather than creating a duplicate — useful for ongoing operations:

ts
// First call creates the entry
context.messages.add({ id: 'my-scan', message: 'Scanning...', level: 'info', status: 'loading' })

// Second call with same id updates it
context.messages.add({ id: 'my-scan', message: 'Scan complete', level: 'success', status: 'idle' })

Toast notifications

Set notify: true to surface the message entry as a toast overlay. Toasts appear whether or not the Messages panel is open.

ts
context.messages.add({
  message: 'URL copied to clipboard',
  level: 'success',
  notify: true,
  autoDismiss: 2000, // disappear after 2 seconds
})

Toast auto-dismiss defaults to 5 seconds.

Managing messages

ts
// Remove a specific message by id
context.messages.remove(entryId)

// Clear all messages
context.messages.clear()

Capacity tops out at 1000 entries; the oldest are dropped automatically when the limit is hit.

The A11y Checker example is a plugin that uses messages to report accessibility violations with severity levels, element positions, and WCAG labels.

Dock badge

The Messages dock icon shows a badge with the total message count and hides itself when there are no messages.

Events

The host emits events for observers of the message stream:

ts
ctx.messages.events.on('message:added', (entry) => { /* ... */ })
ctx.messages.events.on('message:updated', (entry) => { /* ... */ })
ctx.messages.events.on('message:removed', (id) => { /* ... */ })
ctx.messages.events.on('message:cleared', () => { /* ... */ })

Use the events to bridge messages into external tools — mirror them into a structured log, forward certain categories to your own reporter, etc.:

ts
ctx.messages.events.on('message:added', (entry) => {
  if (entry.category === 'a11y')
    console.log('a11y finding:', entry.message)
})

Long-running operation pattern

Combine id-based deduplication with status: 'loading' to drive a single message through a multi-step lifecycle:

ts
async function rebuild(ctx) {
  const handle = await ctx.messages.add({
    id: 'my-plugin:rebuild',
    message: 'Rebuilding...',
    level: 'info',
    status: 'loading',
  })

  try {
    await doRebuild()
    await handle.update({ message: 'Rebuild complete', level: 'success', status: 'idle' })
  }
  catch (error) {
    await handle.update({
      message: 'Rebuild failed',
      level: 'error',
      description: (error as Error).message,
      stacktrace: (error as Error).stack,
      status: 'idle',
    })
  }
}

Released under the MIT License.