Skip to main content

Webviews

Webviews allow you to create fully customizable views within VS Code using HTML, CSS, and JavaScript. They’re perfect for building custom editors, visualizations, and interactive documentation.

Overview

Webviews display HTML content in an isolated iframe-like environment. They can communicate bidirectionally with your extension through message passing.

When to Use Webviews

  • Custom editors for specialized file types
  • Data visualizations and charts
  • Interactive tutorials or documentation
  • Configuration UIs
  • Preview panels for markup languages
Webviews have high memory overhead. Use them sparingly and prefer TreeViews or QuickPick for simpler UIs.

Webview Panels

Webview panels are standalone webviews that appear in the editor area, like tabs.

Creating a Webview Panel

1

Create the panel

Use window.createWebviewPanel to create a webview panel:
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  const disposable = vscode.commands.registerCommand('myExtension.openWebview', () => {
    // Create and show panel
    const panel = vscode.window.createWebviewPanel(
      'myWebview', // Identifies the type of webview
      'My Webview', // Title displayed in tab
      vscode.ViewColumn.One, // Editor column to show the panel in
      {
        // Enable scripts in the webview
        enableScripts: true,
        // Restrict loading resources to specific folders
        localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'media')]
      }
    );

    // Set HTML content
    panel.webview.html = getWebviewContent();
  });

  context.subscriptions.push(disposable);
}

function getWebviewContent(): string {
  return `<!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Webview</title>
  </head>
  <body>
    <h1>Hello from Webview!</h1>
    <button onclick="sendMessage()">Send Message</button>
    
    <script>
      const vscode = acquireVsCodeApi();
      
      function sendMessage() {
        vscode.postMessage({ command: 'hello', text: 'Hello from webview!' });
      }
    </script>
  </body>
  </html>`;
}
2

Handle messages from webview

Listen for messages sent from the webview:
panel.webview.onDidReceiveMessage(
  message => {
    switch (message.command) {
      case 'hello':
        vscode.window.showInformationMessage(message.text);
        return;
      case 'error':
        vscode.window.showErrorMessage(message.text);
        return;
    }
  },
  undefined,
  context.subscriptions
);
3

Send messages to webview

Post messages from your extension to the webview:
// Send message to webview
panel.webview.postMessage({ command: 'update', data: { count: 5 } });

// In webview JavaScript:
// window.addEventListener('message', event => {
//   const message = event.data;
//   if (message.command === 'update') {
//     document.getElementById('count').textContent = message.data.count;
//   }
// });

Loading Local Resources

Webviews cannot directly access local files. You must convert file URIs using asWebviewUri().

Resource Loading Example

function getWebviewContent(
  webview: vscode.Webview,
  extensionUri: vscode.Uri
): string {
  // Get paths to resources
  const styleUri = webview.asWebviewUri(
    vscode.Uri.joinPath(extensionUri, 'media', 'styles.css')
  );
  const scriptUri = webview.asWebviewUri(
    vscode.Uri.joinPath(extensionUri, 'media', 'main.js')
  );
  const imageUri = webview.asWebviewUri(
    vscode.Uri.joinPath(extensionUri, 'media', 'logo.png')
  );

  // Use nonce for Content Security Policy
  const nonce = getNonce();

  return `<!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="Content-Security-Policy" 
          content="default-src 'none'; 
                   style-src ${webview.cspSource}; 
                   img-src ${webview.cspSource} https:; 
                   script-src 'nonce-${nonce}';">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="${styleUri}" rel="stylesheet">
    <title>My Webview</title>
  </head>
  <body>
    <img src="${imageUri}" alt="Logo">
    <h1>Hello World</h1>
    <script nonce="${nonce}" src="${scriptUri}"></script>
  </body>
  </html>`;
}

function getNonce(): string {
  let text = '';
  const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  for (let i = 0; i < 32; i++) {
    text += possible.charAt(Math.floor(Math.random() * possible.length));
  }
  return text;
}
Always use a Content Security Policy (CSP) to restrict what content can be loaded in your webview.

State Persistence

Webviews can persist state across panel closures and VS Code restarts.

Webview State Management

class MyWebviewPanel {
  public static currentPanel: MyWebviewPanel | undefined;
  private readonly _panel: vscode.WebviewPanel;
  private _disposables: vscode.Disposable[] = [];

  private constructor(
    panel: vscode.WebviewPanel,
    extensionUri: vscode.Uri
  ) {
    this._panel = panel;

    // Set initial HTML
    this._update();

    // Listen for when the panel is disposed
    this._panel.onDidDispose(() => this.dispose(), null, this._disposables);

    // Handle messages from webview
    this._panel.webview.onDidReceiveMessage(
      message => {
        switch (message.command) {
          case 'saveState':
            // Persist state
            break;
        }
      },
      null,
      this._disposables
    );
  }

  public static createOrShow(extensionUri: vscode.Uri) {
    const column = vscode.window.activeTextEditor
      ? vscode.window.activeTextEditor.viewColumn
      : undefined;

    // If we already have a panel, show it
    if (MyWebviewPanel.currentPanel) {
      MyWebviewPanel.currentPanel._panel.reveal(column);
      return;
    }

    // Create new panel
    const panel = vscode.window.createWebviewPanel(
      'myWebview',
      'My Webview',
      column || vscode.ViewColumn.One,
      {
        enableScripts: true,
        localResourceRoots: [vscode.Uri.joinPath(extensionUri, 'media')],
        // Keep context when hidden
        retainContextWhenHidden: true
      }
    );

    MyWebviewPanel.currentPanel = new MyWebviewPanel(panel, extensionUri);
  }

  private _update() {
    this._panel.webview.html = this._getHtmlForWebview(this._panel.webview);
  }

  public dispose() {
    MyWebviewPanel.currentPanel = undefined;
    this._panel.dispose();
    while (this._disposables.length) {
      const disposable = this._disposables.pop();
      if (disposable) {
        disposable.dispose();
      }
    }
  }

  private _getHtmlForWebview(webview: vscode.Webview): string {
    return `<!DOCTYPE html>
    <html>
    <body>
      <h1>Persistent Webview</h1>
      <script>
        const vscode = acquireVsCodeApi();
        
        // Get previous state
        const previousState = vscode.getState();
        let count = previousState ? previousState.count : 0;
        
        // Update state
        function increment() {
          count++;
          vscode.setState({ count: count });
          document.getElementById('counter').textContent = count;
        }
        
        // Restore state
        document.getElementById('counter').textContent = count;
      </script>
      <button onclick="increment()">Count: <span id="counter">0</span></button>
    </body>
    </html>`;
  }
}

Webview Serialization

To restore webviews across VS Code restarts, register a serializer.
export function activate(context: vscode.ExtensionContext) {
  // Register serializer
  vscode.window.registerWebviewPanelSerializer('myWebview', {
    async deserializeWebviewPanel(
      webviewPanel: vscode.WebviewPanel,
      state: any
    ) {
      // Restore the panel
      webviewPanel.webview.options = {
        enableScripts: true
      };
      webviewPanel.webview.html = getWebviewContent(webviewPanel.webview, context.extensionUri);
      
      // Restore state if needed
      if (state) {
        webviewPanel.webview.postMessage({ command: 'restoreState', state });
      }
    }
  });
}

Webview Views

Webview views are webviews that appear in the sidebar, panel, or other view containers.

Creating a Webview View

1

Register in package.json

Define your view in package.json:
{
  "contributes": {
    "views": {
      "explorer": [
        {
          "type": "webview",
          "id": "myExtension.myView",
          "name": "My View"
        }
      ]
    }
  }
}
2

Implement the provider

Create a WebviewViewProvider:
class MyWebviewViewProvider implements vscode.WebviewViewProvider {
  constructor(private readonly _extensionUri: vscode.Uri) {}

  resolveWebviewView(
    webviewView: vscode.WebviewView,
    context: vscode.WebviewViewResolveContext,
    token: vscode.CancellationToken
  ) {
    webviewView.webview.options = {
      enableScripts: true,
      localResourceRoots: [this._extensionUri]
    };

    webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);

    // Handle messages
    webviewView.webview.onDidReceiveMessage(data => {
      switch (data.type) {
        case 'action':
          vscode.window.showInformationMessage(data.value);
          break;
      }
    });
  }

  private _getHtmlForWebview(webview: vscode.Webview): string {
    return `<!DOCTYPE html>
    <html>
    <body>
      <h2>Sidebar Webview</h2>
      <button onclick="sendAction()">Click Me</button>
      <script>
        const vscode = acquireVsCodeApi();
        function sendAction() {
          vscode.postMessage({ type: 'action', value: 'Button clicked!' });
        }
      </script>
    </body>
    </html>`;
  }
}
3

Register the provider

Register your provider in the extension’s activate function:
export function activate(context: vscode.ExtensionContext) {
  const provider = new MyWebviewViewProvider(context.extensionUri);
  
  context.subscriptions.push(
    vscode.window.registerWebviewViewProvider('myExtension.myView', provider)
  );
}

Best Practices

Security

Always implement a strict Content Security Policy to prevent XSS attacks:
<meta http-equiv="Content-Security-Policy" 
      content="default-src 'none'; 
               style-src ${webview.cspSource} 'unsafe-inline'; 
               img-src ${webview.cspSource} https:; 
               script-src 'nonce-${nonce}'; 
               font-src ${webview.cspSource};">

Performance

// ❌ Bad: Creates new panel every time
vscode.commands.registerCommand('open', () => {
  vscode.window.createWebviewPanel(...);
});

// ✅ Good: Reuse existing panel
let currentPanel: vscode.WebviewPanel | undefined;
vscode.commands.registerCommand('open', () => {
  if (currentPanel) {
    currentPanel.reveal();
  } else {
    currentPanel = vscode.window.createWebviewPanel(...);
    currentPanel.onDidDispose(() => { currentPanel = undefined; });
  }
});

Message Typing

// Define message types
type Message = 
  | { command: 'update'; data: any }
  | { command: 'error'; error: string };

panel.webview.onDidReceiveMessage((message: Message) => {
  switch (message.command) {
    case 'update':
      handleUpdate(message.data);
      break;
    case 'error':
      handleError(message.error);
      break;
  }
});

Common Patterns

Using Modern Frameworks

You can use React, Vue, or other frameworks in webviews:
function getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri): string {
  const scriptUri = webview.asWebviewUri(
    vscode.Uri.joinPath(extensionUri, 'dist', 'webview.js')
  );

  return `<!DOCTYPE html>
  <html>
  <head>
    <meta charset="UTF-8">
  </head>
  <body>
    <div id="root"></div>
    <script src="${scriptUri}"></script>
  </body>
  </html>`;
}
Build your frontend code separately and load the bundled output in the webview.

Resources

Webview API

Complete webview guide

Webview Sample

Sample extension code

Build docs developers (and LLMs) love