@microsoft/vscode-ext-webview (Preview)

July 29, 2026 · View on GitHub

Preview release. This package is published in preview while the API surface stabilises. Breaking changes may land between minor versions until a 1.0.0 release.

Webview infrastructure for VS Code extensions: type-safe tRPC over postMessage, a one-call front door that opens a panel and wires the transport, optional React hooks for the webview side, and pluggable telemetry.

The transport was extracted from the webview stack that ships in the battle-tested DocumentDB for VS Code and Azure Cosmos DB for VS Code extensions, where it powers the production collection, document, and query experiences that people use every day.

A companion vscode-webview-starter-kit repository was built to make onboarding easy: it is a ready-to-run reference you can use as a template for a brand-new extension, or read alongside this package to see every moving part wired together.


Crossing the webview boundary

A VS Code webview runs in its own isolated context: a sandboxed iframe with no direct access to the extension host, the file system, or the VS Code API. The only way in or out is asynchronous postMessage. That isolation is good for security, but it means every feature has to cross a process-like boundary by hand. You serialize a message, correlate responses with requests, track cancellation, and keep both sides' types in sync as the code changes.

This package turns that boundary into a typed function call. You define a tRPC router once on the extension host, and the webview calls it like a local API with full type inference, autocompletion, and refactor-safety. The transport, request and response correlation, cancellation, and lifecycle wiring are handled for you, so feature code never touches postMessage directly.

Architecture

The extension host owns a vscode.WebviewPanel; the webview runs your React app inside it and talks to the host through tRPC over window.postMessage. There is no HTTP, no WebSocket, and no string-typed protocol to maintain by hand. tRPC types flow from the router definition straight to the React component that calls into it.

Extension Host (Node.js)               Webview (Browser)
+----------------------------+         +----------------------------+
|  openWebview / Controller  |         |  React tree                |
|   |- router (tRPC)         | <-----> |   |- WithWebviewContext    |
|   |- attachTrpc dispatch   |  post   |   |- useTrpcClient         |
|   |- telemetry logger      |  Msg    |   |- useConfiguration      |
|   '- AbortSignal in ctx    |         |   '- vscodeLink transport  |
+----------------------------+         +----------------------------+
     import from ./host                import from ./react | ./webview

The same AppRouter type is shared by both sides: define the router once on the extension host, then call it from the webview with full type inference, auto-completion, and refactor-safety.

Quick start

The shortest path to a working webview is four files: define a router, open the panel with openWebview, render the React tree with WithWebviewContext, and call procedures with useTrpcClient.

For a complete extension layout with build configuration, accessibility helpers, Monaco wiring, and tested-end-to-end command registration, copy the vscode-webview-starter-kit instead of starting from these snippets. The starter kit is the canonical consumer reference; this section is for understanding the moving parts.

1. Install

npm install @microsoft/vscode-ext-webview

The package declares @trpc/client, @trpc/server, react, and vscode-webview as peer dependencies. Bring whatever versions you use yourself; the package will not pull duplicates into your webview bundle. react and vscode-webview are optional peers (see Peer dependencies below): both are used only by the ./react surface, so a host-only consumer or a framework-agnostic ./webview consumer needs neither and gets no missing-peer warning. react-dom is not a peer of this package; it is a transitive concern of any React DOM app shell.

2. Define the router (extension host)

// src/webviews/_integration/appRouter.ts
import { initWebviewTrpc, type BaseRouterContext } from '@microsoft/vscode-ext-webview';
import { z } from 'zod';

export type RouterContext = BaseRouterContext & {
  // application-specific fields, e.g.:
  workspaceRoot: string;
};

export const trpc = initWebviewTrpc<RouterContext>();

export const appRouter = trpc.router({
  hello: trpc.publicProcedure
    .input(z.object({ name: z.string() }))
    .query(({ input }) => ({ greeting: `Hello, ${input.name}!` })),
});

export type AppRouter = typeof appRouter;

initWebviewTrpc<TContext>() returns the tRPC builders bound to your context type: router, publicProcedure (whose ctx is typed as TContext), createCallerFactory (used by the host dispatcher), and middleware. Export the whole instance as trpc and pass it to openWebview (below): the dispatcher reads trpc.createCallerFactory off it, so the caller factory can never be mismatched with your router and there is nothing extra to re-export.

3. Open the panel (extension host)

// src/extension.ts
import * as vscode from 'vscode';
import { openWebview } from '@microsoft/vscode-ext-webview/host';
import { appRouter, trpc, type AppRouter, type RouterContext } from './webviews/_integration/appRouter';

type MyViewConfig = { initialMessage: string };

export function activate(ctx: vscode.ExtensionContext) {
  ctx.subscriptions.push(
    vscode.commands.registerCommand('myExtension.openMyView', () => {
      openWebview<AppRouter, MyViewConfig, RouterContext>(ctx, {
        title: 'My View',
        viewType: 'myView', // matches the React component registration
        router: appRouter,
        trpc,
        context: { workspaceRoot: vscode.workspace.workspaceFolders?.[0].uri.fsPath ?? '' },
        config: { initialMessage: 'ready' } satisfies MyViewConfig,
        sourceLayout: {
          bundled: { dir: '', file: 'views.js' },
          dev: { dir: 'out/src/webviews', file: 'index.js' },
        },
        isBundled: !!process.env.IS_BUNDLE,
        devServerHost: 'http://localhost:18080',
      });
    }),
  );
}

openWebview returns a WebviewController handle exposing panel, onDisposed, revealToForeground, dispose, and isDisposed. It opens the panel, renders the HTML, and wires the tRPC dispatch pump. Procedure activity is logged to the extension-host console out of the box; pass a logger option to route the entries elsewhere. See Observability for logging and Application Insights-style telemetry.

4. Render the view (webview / browser)

The webview entry point exports a render(viewType, vscodeApi) function that the framework's HTML scaffold calls when the panel loads. The viewType argument is the key you passed to openWebview (here, 'myView'); use it to look up the matching React component.

// src/webviews/index.tsx
import { createRoot } from 'react-dom/client';
import { WithWebviewContext, type WebviewState } from '@microsoft/vscode-ext-webview/react';
import type { WebviewApi } from 'vscode-webview';
import { MyView } from './myView/MyView';

const registry = {
  myView: MyView,
} as const;

export function render(viewType: keyof typeof registry, vscodeApi: WebviewApi<WebviewState>) {
  const Component = registry[viewType];
  createRoot(document.getElementById('root')!).render(
    <WithWebviewContext vscodeApi={vscodeApi}>
      <Component />
    </WithWebviewContext>,
  );
}
// src/webviews/myView/MyView.tsx
import { useEffect, useState } from 'react';
import { useConfiguration, useTrpcClient } from '@microsoft/vscode-ext-webview/react';
import type { AppRouter } from '../_integration/appRouter';

type MyViewConfig = { initialMessage: string };

export const MyView = () => {
  const config = useConfiguration<MyViewConfig>();
  const trpcClient = useTrpcClient<AppRouter>();
  const [greeting, setGreeting] = useState(config.initialMessage);

  useEffect(() => {
    void trpcClient.hello.query({ name: 'world' }).then((r) => setGreeting(r.greeting));
  }, [trpcClient]);

  return <h1>{greeting}</h1>;
};

That is the complete data path: the React component calls trpcClient.hello.query(...), the call travels through vscodeLink as a postMessage, the host dispatches it to appRouter.hello, the result is sent back with postMessage, and the call promise resolves with full type inference for r.greeting.

useTrpcClient<AppRouter>() returns the client directly. The client (and the event channel from useRpcEvents()) is shared per webview: every component that calls the hook receives the same instance, so there is no provider tree to wire up and cross-cutting observers see every call.

Behind the scenes (advanced, optional)

The factory is the front door, but every layer underneath it is a public, documented primitive. Reach for these when you outgrow the one-call path. Each is covered in depth in ADVANCED.md.

  • Bring your own panel. attachTrpc(panel, ctx, router, callerFactory?, logger?) wires the tRPC dispatch pump onto a vscode.WebviewPanel you already own, without WebviewController.
  • Framework-agnostic webview client. connectTrpc(vscodeApi, options?) from ./webview builds a typed client plus an event channel with no React dependency, so you can bind another UI framework on top of the transport.
  • Webview-side error and event observer. createEventChannel() and the errorLink tRPC link surface success, error, and aborted events to a single place (an announcer, a toaster, telemetry).
  • Pluggable telemetry. telemetryMiddlewareBody plus a TelemetryRunner routes per-call telemetry into your own analytics (for example Application Insights); loggingMiddlewareBody and ProcedureLogger cover console-style logging.
  • Push events from host to webview. TypedEventSink<T> bridges push-style producers (VS Code event emitters, driver callbacks) into tRPC subscriptions.
  • The type-only router import rule. Webview code imports only import type { AppRouter }, keeping extension-host code out of the browser bundle.

See ADVANCED.md for the full manual, including a single shared client, worked telemetry adapters, the event channel, push events, and the host/browser import boundary.

Observability

The transport reports what it is doing through logging (console / output, for humans) and telemetry (structured analytics, for dashboards). Logging comes in two flavours — one on the webview side, one on the extension-host side — and telemetry is a separate, policy-driven layer. Everything except the host dispatch logger's console default is opt-in, so you enable exactly what you need.

Webview-side request logging

The shared webview client can log every query / mutation / subscription to the webview devtools console using tRPC's loggerLink — a rich, grouped, color-coded view of each call's input and result. It is off by default so a production webview stays quiet.

Enable it once, at the React root, via WithWebviewContext:

import { WithWebviewContext } from '@microsoft/vscode-ext-webview/react';

<WithWebviewContext vscodeApi={vscodeApi} enableRpcLogging>
  <App />
</WithWebviewContext>;

For the framework-agnostic client, pass the flag directly:

const { client } = connectTrpc<AppRouter>(vscodeApi, { logger: true });

Opening the webview console from VS Code: run Developer: Open Webview Developer Tools from the Command Palette (with the webview focused) to open the Chromium devtools for that webview; the tRPC log appears on the Console tab. When you launch the extension with a debugger attached this is the webview's own console — distinct from the extension-host Debug Console, where the dispatch logger below writes.

Webview-side observer errors

Cross-cutting observers on the event channel (useRpcEvents() / connectTrpc's events) are isolated: if one of your onSuccess / onError / onAborted handlers throws, the channel catches it so a broken observer can never break the RPC it was only watching. The isolated error goes to an onObserverError sink that defaults to console.error. Route it to telemetry when you want observer failures tracked — off by default so you are not opted into events you may not want:

<WithWebviewContext vscodeApi={vscodeApi} onObserverError={(error, { info, phase }) => report(info.path, phase, error)}>
  <App />
</WithWebviewContext>

See ADVANCED.md for the full contract.

Extension-host dispatch logging

The host dispatcher logs one structured entry per completed query, mutation, and subscription ([tRPC] <type> <path> (<ms>) <status>). It is on by default via consoleProcedureLogger, writing to the extension-host console — the Extension Host output channel, or your debugger's Debug Console when you run the extension under a debugger.

Route those entries elsewhere by passing your own ProcedureLogger as the logger option. The option is named logger, not telemetry, because it is log plumbing; for analytics see the next section.

import { openWebview, type ProcedureLogger } from '@microsoft/vscode-ext-webview/host';

const channel = vscode.window.createOutputChannel('My View');
const logger: ProcedureLogger = {
  onEnd: (e) => channel.appendLine(`${e.type} ${e.path} ${e.durationMs}ms ${e.ok ? 'ok' : 'FAIL'}`),
};

openWebview(ctx, {
  /* …title, viewType, router, context, config, sourceLayout… */
  logger,
});

Telemetry (Application Insights via @microsoft/vscode-azext-utils)

For structured analytics, wire the instance-agnostic telemetryMiddlewareBody onto your procedures with a TelemetryRunner adapter. The body is a thin delegator: it resolves the telemetry event id and hands control to your runner. The runner establishes the telemetry scope, contributes whatever it likes to the procedure context via invoke(enrichment), and classifies the outcome from the returned result. Most VS Code extensions route this through callWithTelemetryAndErrorHandling, which records duration for free:

import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils';
import { initWebviewTrpc } from '@microsoft/vscode-ext-webview';
import { getInvocationSignal, telemetryMiddlewareBody, type TelemetryRunner } from '@microsoft/vscode-ext-webview/host';

const runner: TelemetryRunner<{ actionContext: IActionContext }> = {
  async run(eventId, invocation, invoke) {
    const result = await callWithTelemetryAndErrorHandling(eventId, async (actionContext) => {
      actionContext.errorHandling.suppressDisplay = true;
      const middlewareResult = await invoke({ actionContext }); // procedures read ctx.actionContext
      const aborted = getInvocationSignal(invocation.ctx)?.aborted ?? false;
      if (aborted) actionContext.telemetry.properties.result = 'Canceled';
      else if (!middlewareResult.ok && middlewareResult.error) {
        actionContext.telemetry.properties.result = 'Failed';
        actionContext.telemetry.properties.error = middlewareResult.error.name ?? '';
      }
      return middlewareResult;
    });
    if (!result) throw new Error(`no telemetry result for ${eventId}`);
    return result;
  },
};

const { publicProcedure } = initWebviewTrpc<RouterContext>();
// Build your router from `tracked` instead of the bare `publicProcedure`:
export const tracked = publicProcedure.use(
  telemetryMiddlewareBody(runner, { buildEventId: ({ type, path }) => `myExt.rpc.${type}.${path}` }),
);

Inside a procedure, read the fields your runner contributed (here ctx.actionContext) to add properties/measurements. Declare that field on your router context type so procedures read it without a telemetry-specific cast — see ADVANCED.md for the full worked adapter, and MIGRATION.md if you are upgrading from an earlier version.

Entry points

The package has four entry points so bundlers do not drag Node / VS Code APIs into the webview bundle, and so a non-React consumer never pulls React in.

SubpathSideImportsKey exports
.shared (side-agnostic)no vscode, no ReactinitWebviewTrpc, BaseRouterContext, TypedEventSink, wire-protocol message types
./hostextension host (Node.js)fs, path, vscodeopenWebview, WebviewController, attachTrpc, telemetryMiddlewareBody, loggingMiddlewareBody, consoleProcedureLogger
./webviewwebview (browser), any frameworkno ReactconnectTrpc, createEventChannel, vscodeLink, errorLink
./reactwebview (browser), ReactReactuseTrpcClient, useRpcEvents, useConfiguration, WithWebviewContext
// Shared. Safe to import from either side.
import { initWebviewTrpc, TypedEventSink, type BaseRouterContext } from '@microsoft/vscode-ext-webview';

// Extension host side. Uses fs, path, vscode.
import { openWebview, WebviewController, attachTrpc } from '@microsoft/vscode-ext-webview/host';

// Webview, framework-agnostic. No React.
import { connectTrpc, createEventChannel, vscodeLink } from '@microsoft/vscode-ext-webview/webview';

// Webview, React hooks.
import { useTrpcClient, useConfiguration, WithWebviewContext } from '@microsoft/vscode-ext-webview/react';

What's inside

  • openWebview / WebviewController: open a vscode.WebviewPanel, dispatch incoming tRPC operations (queries, mutations, subscriptions), and handle abort / subscription cancellation lifecycle. The factory is sugar over the controller's options-bag constructor.
  • attachTrpc: the dispatch primitive the controller is built on; wire tRPC onto a panel you already own.
  • initWebviewTrpc: the typed tRPC initialiser. Returns router, publicProcedure, createCallerFactory, and middleware bound to your context type.
  • connectTrpc / createEventChannel: the framework-agnostic webview client and its observable event channel.
  • vscodeLink: a custom tRPC link that bridges tRPC over window.postMessage, type-safe end to end.
  • errorLink: an optional tRPC link that forwards query / mutation errors to a consumer-supplied handler (announce, toast, telemetry) without preventing the normal error flow.
  • TypedEventSink<T>: a small typed async-iterable that bridges push-style domain events into tRPC subscriptions.
  • React hooks: useTrpcClient, useRpcEvents, useConfiguration, and the WithWebviewContext provider.
  • Pluggable telemetry: telemetryMiddlewareBody + TelemetryRunner and loggingMiddlewareBody + ProcedureLogger, with consoleProcedureLogger as the zero-config default.

Peer dependencies

PackageVersionOptional?
@trpc/client^11.0.0Required — core transport
@trpc/server^11.0.0Required — core transport
react>=18.0.0Optional — only for the ./react surface
vscode-webview^1.0.0Optional — only for the ./react surface

react and vscode-webview are declared optional via peerDependenciesMeta, so a consumer that only uses ./host (or the framework-agnostic ./webview) surface installs neither and sees no missing-peer warning. Both peers are referenced only by the ./react surface (vscode-webview supplies the WebviewApi type used in WithWebviewContext); the ./webview transport defines its own structural VsCodeApiLike instead, so it has no vscode-webview dependency.

Scope

This package ships only the webview transport (tRPC over postMessage), the panel facade, and the minimum React glue to consume it. UI components, UX policy (context-menu handling, focus management, and so on), accessibility helpers, editor-specific behaviours, and other consumer concerns are out of scope by design. Keep them in your application repository or pick dedicated libraries for them.

Starter kit and reference consumers

The recommended way to start a new consumer is to copy the vscode-webview-starter-kit and adapt it. The starter kit covers things the package intentionally does not own: webpack / Vite build configuration for both the extension and the views bundle; accessibility helpers (an ARIA announcer, a selective context-menu prevention hook); a Monaco editor integration recipe; and a worked demo view exercising the tRPC client end to end.

A consumer-side integration layer (router + telemetry runner + configuration knobs) typically lives in a folder like src/webviews/_integration/. The underscore prefix sorts the folder above feature folders in the file explorer, the conventional "infrastructure / not feature code" signal. The vscode-documentdb and Azure Cosmos DB for VS Code extensions are working examples of that layout against this package.

Status

0.10.1. APIs are subject to change while the package is in preview. See ADVANCED.md for the full set of primitives and patterns.

Contributors

This package and the vscode-webview-starter-kit built alongside it were a team effort:

  • tnaum-ms extracted the package from the webview stack that ships in DocumentDB for VS Code, shaped the tRPC integration and public API, and built the companion starter kit.
  • bk201- built the dynamic theming system and, with sevoku, test-drove the package in Azure Cosmos DB for VS Code, helping show the path toward a more modular design.
  • guanzhousongmicrosoft built the npm release pipelines that publish the package.
  • sevoku helped test-drive the package in Azure Cosmos DB for VS Code with bk201-, surfacing the modular direction.

Thanks to everyone who contributed ideas, reviews, and feedback along the way.

License

MIT. See LICENSE (shipped with the package); the repository-wide copy lives at the repository root.