@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.0release.
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 avscode.WebviewPanelyou already own, withoutWebviewController. - Framework-agnostic webview client.
connectTrpc(vscodeApi, options?)from./webviewbuilds 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 theerrorLinktRPC link surface success, error, and aborted events to a single place (an announcer, a toaster, telemetry). - Pluggable telemetry.
telemetryMiddlewareBodyplus aTelemetryRunnerroutes per-call telemetry into your own analytics (for example Application Insights);loggingMiddlewareBodyandProcedureLoggercover 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.
| Subpath | Side | Imports | Key exports |
|---|---|---|---|
. | shared (side-agnostic) | no vscode, no React | initWebviewTrpc, BaseRouterContext, TypedEventSink, wire-protocol message types |
./host | extension host (Node.js) | fs, path, vscode | openWebview, WebviewController, attachTrpc, telemetryMiddlewareBody, loggingMiddlewareBody, consoleProcedureLogger |
./webview | webview (browser), any framework | no React | connectTrpc, createEventChannel, vscodeLink, errorLink |
./react | webview (browser), React | React | useTrpcClient, 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 avscode.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. Returnsrouter,publicProcedure,createCallerFactory, andmiddlewarebound to your context type.connectTrpc/createEventChannel: the framework-agnostic webview client and its observable event channel.vscodeLink: a custom tRPC link that bridges tRPC overwindow.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 theWithWebviewContextprovider. - Pluggable telemetry:
telemetryMiddlewareBody+TelemetryRunnerandloggingMiddlewareBody+ProcedureLogger, withconsoleProcedureLoggeras the zero-config default.
Peer dependencies
| Package | Version | Optional? |
|---|---|---|
@trpc/client | ^11.0.0 | Required — core transport |
@trpc/server | ^11.0.0 | Required — core transport |
react | >=18.0.0 | Optional — only for the ./react surface |
vscode-webview | ^1.0.0 | Optional — 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.