Skip to content
Back to skills

2515 Webview Patterns 2750a5d4

ASecurity

Comprehensive guide for building webview UIs in VS Code extensions.

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 11, 2026
developmentjavascripttypescriptjavareactnodeapisecurity

Works with

  • cursor
  • vscode
  • cli
  • api

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 2515-webview-patterns_2750a5d4 --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 2515 Webview Patterns 2750a5d4?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 2515 Webview Patterns 2750a5d4
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-2515-webview-patterns-2750a5d4/badge)](https://www.skillsdirectory.com/skills/tools-only-2515-webview-patterns-2750a5d4)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
# Webview Patterns

Comprehensive guide for building webview UIs in VS Code extensions.

## Table of Contents

- [Creating Webviews](#creating-webviews)
- [Content Security Policy](#content-security-policy)
- [Resource Loading](#resource-loading)
- [Message Passing](#message-passing)
- [State Persistence](#state-persistence)
- [React Integration](#react-integration)
- [Vite Configuration](#vite-configuration)
- [Sidebar Webviews](#sidebar-webviews)

---

## Creating Webviews

### WebviewPanel (Editor Area)

```typescript
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('myExt.openPanel', () => {
      const panel = vscode.window.createWebviewPanel(
        'myWebview',           // viewType (unique ID)
        'My Panel',            // title
        vscode.ViewColumn.One, // editor column
        {
          enableScripts: true,                    // Allow JS
          retainContextWhenHidden: true,          // Keep state when hidden
          localResourceRoots: [                   // Allowed resource paths
            vscode.Uri.joinPath(context.extensionUri, 'dist')
          ]
        }
      );

      panel.webview.html = getWebviewContent(panel.webview, context.extensionUri);

      // Handle messages from webview
      panel.webview.onDidReceiveMessage(
        message => handleMessage(message, panel),
        undefined,
        context.subscriptions
      );

      // Handle panel disposal
      panel.onDidDispose(() => {
        // Cleanup resources
      });
    })
  );
}
```

### WebviewViewProvider (Sidebar)

```typescript
class MyWebviewProvider implements vscode.WebviewViewProvider {
  private _view?: vscode.WebviewView;

  constructor(private readonly extensionUri: vscode.Uri) {}

  resolveWebviewView(
    webviewView: vscode.WebviewView,
    _context: vscode.WebviewViewResolveContext,
    _token: vscode.CancellationToken
  ) {
    this._view = webviewView;

    webviewView.webview.options = {
      enableScripts: true,
      localResourceRoots: [this.extensionUri]
    };

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

    webviewView.webview.onDidReceiveMessage(msg => {
      // Handle messages
    });
  }

  private getHtml(webview: vscode.Webview): string {
    // Return HTML content
  }

  public postMessage(message: any) {
    this._view?.webview.postMessage(message);
  }
}

// Register in activate()
const provider = new MyWebviewProvider(context.extensionUri);
context.subscriptions.push(
  vscode.window.registerWebviewViewProvider('myExt.sidebarView', provider)
);
```

---

## Content Security Policy

Always set a strict CSP to prevent XSS attacks.

```typescript
function getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri): string {
  const scriptUri = webview.asWebviewUri(
    vscode.Uri.joinPath(extensionUri, 'dist', 'webview.js')
  );
  const styleUri = webview.asWebviewUri(
    vscode.Uri.joinPath(extensionUri, 'dist', 'webview.css')
  );

  // Generate nonce for inline scripts
  const nonce = getNonce();

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

function getNonce(): string {
  let text = '';
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  for (let i = 0; i < 32; i++) {
    text += chars.charAt(Math.floor(Math.random() * chars.length));
  }
  return text;
}
```

**CSP Directives:**

| Directive | Purpose |
|-----------|---------|
| `default-src 'none'` | Block all by default |
| `script-src 'nonce-xxx'` | Only allow scripts with matching nonce |
| `style-src ${webview.cspSource}` | Allow extension styles |
| `font-src ${webview.cspSource}` | Allow extension fonts |
| `img-src ${webview.cspSource} https:` | Allow extension and HTTPS images |

---

## Resource Loading

Convert local paths to webview URIs:

```typescript
// Extension code
const scriptUri = webview.asWebviewUri(
  vscode.Uri.joinPath(extensionUri, 'dist', 'webview', 'index.js')
);

const imageUri = webview.asWebviewUri(
  vscode.Uri.joinPath(extensionUri, 'media', 'icon.png')
);
```

**In HTML:**

```html
<script src="${scriptUri}"></script>
<img src="${imageUri}">
<link href="${styleUri}" rel="stylesheet">
```

---

## Message Passing

### Extension → Webview

```typescript
// Extension code
panel.webview.postMessage({
  type: 'update',
  data: { items: [...] }
});
```

### Webview → Extension

```typescript
// Extension code
panel.webview.onDidReceiveMessage(message => {
  switch (message.type) {
    case 'save':
      saveData(message.data);
      break;
    case 'alert':
      vscode.window.showInformationMessage(message.text);
      break;
    case 'openFile':
      vscode.commands.executeCommand('vscode.open', vscode.Uri.file(message.path));
      break;
  }
});
```

### Webview JavaScript

```typescript
// Webview code (runs in browser context)
declare function acquireVsCodeApi(): {
  postMessage(message: any): void;
  getState(): any;
  setState(state: any): void;
};

const vscode = acquireVsCodeApi();

// Send message to extension
function sendToExtension(type: string, data: any) {
  vscode.postMessage({ type, data });
}

// Receive messages from extension
window.addEventListener('message', event => {
  const message = event.data;
  switch (message.type) {
    case 'update':
      updateUI(message.data);
      break;
  }
});

// Example: button click
document.getElementById('saveBtn')?.addEventListener('click', () => {
  vscode.postMessage({ type: 'save', data: getData() });
});
```

---

## State Persistence

Webview state persists when the panel is hidden (if `retainContextWhenHidden: true`) or across VS Code restarts.

```typescript
// Webview code
const vscode = acquireVsCodeApi();

// Restore previous state
const previousState = vscode.getState();
if (previousState) {
  restoreUI(previousState);
}

// Save state when it changes
function updateState(newState: any) {
  vscode.setState(newState);
}

// Example: save form data
document.getElementById('input')?.addEventListener('input', (e) => {
  const value = (e.target as HTMLInputElement).value;
  vscode.setState({ ...vscode.getState(), inputValue: value });
});
```

### Serialization for Restart

```typescript
// Extension code
class MySerializer implements vscode.WebviewPanelSerializer {
  constructor(private readonly extensionUri: vscode.Uri) {}

  async deserializeWebviewPanel(panel: vscode.WebviewPanel, state: any) {
    panel.webview.options = { enableScripts: true };
    panel.webview.html = getWebviewContent(panel.webview, this.extensionUri);

    // Restore state
    if (state) {
      panel.webview.postMessage({ type: 'restore', data: state });
    }
  }
}

// Register in activate()
vscode.window.registerWebviewPanelSerializer('myWebview', new MySerializer(context.extensionUri));
```

---

## React Integration

### Project Structure

```
src/
├── extension.ts          # Extension entry
└── webview/
    ├── index.tsx         # React entry
    ├── App.tsx           # Main component
    ├── components/       # React components
    ├── hooks/
    │   └── useVsCode.ts  # VS Code API hook
    └── vite.config.ts    # Vite config
```

### VS Code Hook

```typescript
// src/webview/hooks/useVsCode.ts
import { useEffect, useRef, useState, useCallback } from 'react';

interface VsCodeApi {
  postMessage(message: any): void;
  getState(): any;
  setState(state: any): void;
}

declare function acquireVsCodeApi(): VsCodeApi;

const vscodeApi = acquireVsCodeApi();

export function useVsCode<T = any>() {
  const [state, setStateInternal] = useState<T | undefined>(vscodeApi.getState);

  const setState = useCallback((newState: T) => {
    vscodeApi.setState(newState);
    setStateInternal(newState);
  }, []);

  const postMessage = useCallback((message: any) => {
    vscodeApi.postMessage(message);
  }, []);

  return { state, setState, postMessage };
}

export function useMessage<T = any>(handler: (message: T) => void) {
  const handlerRef = useRef(handler);
  handlerRef.current = handler;

  useEffect(() => {
    const listener = (event: MessageEvent) => {
      handlerRef.current(event.data);
    };
    window.addEventListener('message', listener);
    return () => window.removeEventListener('message', listener);
  }, []);
}
```

### React App

```tsx
// src/webview/App.tsx
import { useState } from 'react';
import { useVsCode, useMessage } from './hooks/useVsCode';

interface AppState {
  items: string[];
}

interface Message {
  type: 'update' | 'clear';
  data?: any;
}

export function App() {
  const { state, setState, postMessage } = useVsCode<AppState>();
  const [items, setItems] = useState<string[]>(state?.items || []);

  useMessage<Message>((message) => {
    switch (message.type) {
      case 'update':
        setItems(message.data.items);
        setState({ items: message.data.items });
        break;
      case 'clear':
        setItems([]);
        setState({ items: [] });
        break;
    }
  });

  const handleAdd = () => {
    postMessage({ type: 'addItem' });
  };

  const handleSave = () => {
    postMessage({ type: 'save', data: { items } });
  };

  return (
    <div className="app">
      <button onClick={handleAdd}>Add Item</button>
      <button onClick={handleSave}>Save</button>
      <ul>
        {items.map((item, i) => (
          <li key={i}>{item}</li>
        ))}
      </ul>
    </div>
  );
}
```

### Entry Point

```tsx
// src/webview/index.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';
import './styles.css';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>
);
```

---

## Vite Configuration

```typescript
// src/webview/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';

export default defineConfig({
  plugins: [react()],
  build: {
    outDir: resolve(__dirname, '../../dist/webview'),
    rollupOptions: {
      input: resolve(__dirname, 'index.html'),
      output: {
        entryFileNames: '[name].js',
        chunkFileNames: '[name].js',
        assetFileNames: '[name].[ext]'
      }
    },
    sourcemap: true,
    emptyOutDir: true
  },
  define: {
    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production')
  }
});
```

### Webview HTML Template

```html
<!-- src/webview/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Webview</title>
</head>
<body>
  <div id="root"></div>
  <script type="module" src="./index.tsx"></script>
</body>
</html>
```

**Note:** The extension code replaces this with the CSP-secured version at runtime.

---

## Sidebar Webviews

Register in `package.json`:

```json
{
  "contributes": {
    "viewsContainers": {
      "activitybar": [
        {
          "id": "myExtContainer",
          "title": "My Extension",
          "icon": "resources/icon.svg"
        }
      ]
    },
    "views": {
      "myExtContainer": [
        {
          "id": "myExt.sidebarView",
          "name": "My View",
          "type": "webview"
        }
      ]
    }
  }
}
```

The `WebviewViewProvider` (shown earlier) handles the sidebar webview.

---

## VS Code Theme Integration

Use VS Code CSS variables for consistent theming:

```css
/* Webview CSS */
body {
  background-color: var(--vscode-editor-background);
  color: var(--vscode-editor-foreground);
  font-family: var(--vscode-font-family);
  font-size: var(--vscode-font-size);
}

button {
  background-color: var(--vscode-button-background);
  color: var(--vscode-button-foreground);
  border: none;
  padding: 6px 14px;
  cursor: pointer;
}

button:hover {
  background-color: var(--vscode-button-hoverBackground);
}

input, textarea {
  background-color: var(--vscode-input-background);
  color: var(--vscode-input-foreground);
  border: 1px solid var(--vscode-input-border);
  padding: 4px 8px;
}

a {
  color: var(--vscode-textLink-foreground);
}

.error {
  color: var(--vscode-errorForeground);
}

.warning {
  color: var(--vscode-editorWarning-foreground);
}
```

**Common Variables:**

| Variable | Purpose |
|----------|---------|
| `--vscode-editor-background` | Main background |
| `--vscode-editor-foreground` | Main text |
| `--vscode-button-background` | Button background |
| `--vscode-button-foreground` | Button text |
| `--vscode-input-background` | Input background |
| `--vscode-input-border` | Input border |
| `--vscode-focusBorder` | Focus outline |
| `--vscode-list-activeSelectionBackground` | Selected item |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.