Webview API
The webview API allows extensions to create fully customizable views within Visual Studio Code. For example, the built-in Markdown extension uses webviews to render Markdown previews. Webviews can also be used to build complex user interfaces beyond what VS Code's native APIs support.
Think of a webview as an iframe
within VS Code that your extension controls. A webview can render almost any HTML content in this frame, and it communicates with extensions using message passing. This freedom makes webviews incredibly powerful, and opens up a whole new range of extension possibilities.
Webviews are used in several VS Code APIs:
- With Webview Panels created using
createWebviewPanel
. In this case, Webview panels are shown in VS Code as distinct editors. This makes them useful for displaying custom UI and custom visualizations. - As the view for a custom editor. Custom editors allow extensions to provide a custom UI for editing any file in the workspace. The custom editor API also lets your extension hook into editor events such as undo and redo, as well as file events such as save.
- In Webview views that are rendered in the sidebar or panel areas. See the webview view sample extension for more details.
This page focuses on the basic webview panel API, although almost everything covered here applies to the webviews used in custom editors and webview views as well. Even if you are more interested in those APIs, we recommend reading through this page first to familiarize yourself with the webview basics.
Links
VS Code API Usage
Should I use a webview?
Webviews are pretty amazing, but they should also be used sparingly and only when VS Code's native API is inadequate. Webviews are resource heavy and run in a separate context from normal extensions. A poorly designed webview can also easily feel out of place within VS Code.
Before using a webview, please consider the following:
-
Does this functionality really need to live within VS Code? Would it be better as a separate application or website?
-
Is a webview the only way to implement your feature? Can you use the regular VS Code APIs instead?
-
Will your webview add enough user value to justify its high resource cost?
Remember: Just because you can do something with webviews, doesn't mean you should. However, if you are confident that you need to use webviews, then this document is here to help. Let's get started.
Webviews API basics
To explain the webview API, we are going to build a simple extension called Cat Coding. This extension will use a webview to show a gif of a cat writing some code (presumably in VS Code). As we work through the API, we'll continue adding functionality to the extension, including a counter that keeps track of how many lines of source code our cat has written and notifications that inform the user when the cat introduces a bug.
Here's the package.json
for the first version of the Cat Coding extension. You can find the complete code for the example app here. The first version of our extension contributes a command called catCoding.start
. When a user invokes this command, we will show a simple webview with our cat in it. Users will be able to invoke this command from the Command Palette as Cat Coding: Start new cat coding session or even create a keybinding for it if they are so inclined.
{
"name": "cat-coding",
"description": "Cat Coding",
"version": "0.0.1",
"publisher": "bierner",
"engines": {
"vscode": "^1.74.0"
},
"activationEvents": [],
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "catCoding.start",
"title": "Start new cat coding session",
"category": "Cat Coding"
}
]
},
"scripts": {
"vscode:prepublish": "tsc -p ./",
"compile": "tsc -watch -p ./",
"postinstall": "node ./node_modules/vscode/bin/install"
},
"dependencies": {
"vscode": "*"
},
"devDependencies": {
"@types/node": "^9.4.6",
"typescript": "^2.8.3"
}
}
Note: If your extension targets a VS Code version prior to 1.74, you must explicitly list
onCommand:catCoding.start
inactivationEvents
.
Now let's implement the catCoding.start
command. In our extension's main file, we register the catCoding.start
command and use it to show a basic webview:
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
// Create and show a new webview
const panel = vscode.window.createWebviewPanel(
'catCoding', // Identifies the type of the webview. Used internally
'Cat Coding', // Title of the panel displayed to the user
vscode.ViewColumn.One, // Editor column to show the new webview panel in.
{} // Webview options. More on these later.
);
})
);
}
The vscode.window.createWebviewPanel
function creates and shows a webview in the editor. Here is what you see if you try running the catCoding.start
command in its current state:
Our command opens a new webview panel with the correct title, but with no content! To add our cat to new panel, we also need to set the HTML content of the webview using webview.html
:
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
// Create and show panel
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{}
);
// And set its HTML content
panel.webview.html = getWebviewContent();
})
);
}
function getWebviewContent() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
</body>
</html>`;
}
If you run the command again, now the webview looks like this:
Progress!
webview.html
should always be a complete HTML document. HTML fragments or malformed HTML may cause unexpected behavior.
Updating webview content
webview.html
can also update a webview's content after it has been created. Let's use this to make Cat Coding more dynamic by introducing a rotation of cats:
import * as vscode from 'vscode';
const cats = {
'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif',
};
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{}
);
let iteration = 0;
const updateWebview = () => {
const cat = iteration++ % 2 ? 'Compiling Cat' : 'Coding Cat';
panel.title = cat;
panel.webview.html = getWebviewContent(cat);
};
// Set initial content
updateWebview();
// And schedule updates to the content every second
setInterval(updateWebview, 1000);
})
);
}
function getWebviewContent(cat: keyof typeof cats) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<img src="${cats[cat]}" width="300" />
</body>
</html>`;
}
Setting webview.html
replaces the entire webview content, similar to reloading an iframe. This is important to remember once you start using scripts in a webview, since it means that setting webview.html
also resets the script's state.
The example above also uses webview.title
to change the title of the document displayed in the editor. Setting the title does not cause the webview to be reloaded.
Lifecycle
Webview panels are owned by the extension that creates them. The extension must hold onto the webview returned from createWebviewPanel
. If your extension loses this reference, it cannot regain access to that webview again, even though the webview will continue to show in VS Code.
As with text editors, a user can also close a webview panel at any time. When a webview panel is closed by the user, the webview itself is destroyed. Attempting to use a destroyed webview throws an exception. This means that the example above using setInterval
actually has an important bug: if the user closes the panel, setInterval
will continue to fire, which will try to update panel.webview.html
, which of course will throw an exception. Cats hate exceptions. Let's fix this!
The onDidDispose
event is fired when a webview is destroyed. We can use this event to cancel further updates and clean up the webview's resources:
import * as vscode from 'vscode';
const cats = {
'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif',
};
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{}
);
let iteration = 0;
const updateWebview = () => {
const cat = iteration++ % 2 ? 'Compiling Cat' : 'Coding Cat';
panel.title = cat;
panel.webview.html = getWebviewContent(cat);
};
updateWebview();
const interval = setInterval(updateWebview, 1000);
panel.onDidDispose(
() => {
// When the panel is closed, cancel any future updates to the webview content
clearInterval(interval);
},
null,
context.subscriptions
);
})
);
}
Extensions can also programmatically close webviews by calling dispose()
on them. If, for example, we wanted to restrict our cat's workday to five seconds:
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{}
);
panel.webview.html = getWebviewContent('Coding Cat');
// After 5sec, programmatically close the webview panel
const timeout = setTimeout(() => panel.dispose(), 5000);
panel.onDidDispose(
() => {
// Handle user closing panel before the 5sec have passed
clearTimeout(timeout);
},
null,
context.subscriptions
);
})
);
}
Visibility and Moving
When a webview panel is moved into a background tab, it becomes hidden. It is not destroyed however. VS Code will automatically restore the webview's content from webview.html
when the panel is brought to the foreground again:
The .visible
property tells you if the webview panel is currently visible or not.
Extensions can programmatically bring a webview panel to the foreground by calling reveal()
. This method takes an optional target view column to show the panel in. A webview panel may only show in a single editor column at a time. Calling reveal()
or dragging a webview panel to a new editor column moves the webview into that new column.
Let's update our extension to only allow a single webview to exist at a time. If the panel is in the background, then the catCoding.start
command will bring it to the foreground:
export function activate(context: vscode.ExtensionContext) {
// Track the current panel with a webview
let currentPanel: vscode.WebviewPanel | undefined = undefined;
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const columnToShowIn = vscode.window.activeTextEditor
? vscode.window.activeTextEditor.viewColumn
: undefined;
if (currentPanel) {
// If we already have a panel, show it in the target column
currentPanel.reveal(columnToShowIn);
} else {
// Otherwise, create a new panel
currentPanel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
columnToShowIn || vscode.ViewColumn.One,
{}
);
currentPanel.webview.html = getWebviewContent('Coding Cat');
// Reset when the current panel is closed
currentPanel.onDidDispose(
() => {
currentPanel = undefined;
},
null,
context.subscriptions
);
}
})
);
}
Here's the new extension in action:
Whenever a webview's visibility changes, or when a webview is moved into a new column, the onDidChangeViewState
event is fired. Our extension can use this event to change cats based on which column the webview is showing in:
const cats = {
'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif',
'Testing Cat': 'https://media.giphy.com/media/3oriO0OEd9QIDdllqo/giphy.gif',
};
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{}
);
panel.webview.html = getWebviewContent('Coding Cat');
// Update contents based on view state changes
panel.onDidChangeViewState(
(e) => {
const panel = e.webviewPanel;
switch (panel.viewColumn) {
case vscode.ViewColumn.One:
updateWebviewForCat(panel, 'Coding Cat');
return;
case vscode.ViewColumn.Two:
updateWebviewForCat(panel, 'Compiling Cat');
return;
case vscode.ViewColumn.Three:
updateWebviewForCat(panel, 'Testing Cat');
return;
}
},
null,
context.subscriptions
);
})
);
}
function updateWebviewForCat(panel: vscode.WebviewPanel, catName: keyof typeof cats) {
panel.title = catName;
panel.webview.html = getWebviewContent(catName);
}
Inspecting and debugging webviews
The Developer: Toggle Developer Tools command opens a Developer Tools window that you can use debug and inspect your webviews.
Note that if you are using a version of VS Code older than 1.56, or if you are trying to debug a webview that sets enableFindWidget
, you must instead use the Developer: Open Webview Developer Tools command. This command opens a dedicated Developer Tools page for each webview instead of using a Developer Tools page that is shared by all webviews and the editor itself.
From the Developer Tools, you can start inspecting the contents of your webview using the inspect tool located in the top left corner of the Developer Tools window:
You can also view all of the errors and logs from your webview in the developer tools console:
To evaluate an expression in the context of your webview, make sure to select the active frame environment from the dropdown in the top left corner of the Developer tools console panel:
The active frame environment is where the webview scripts themselves are executed.
In addition, the Developer: Reload Webview command reloads all active webviews. This can be helpful if you need to reset a webview's state, or if some webview content on disk has changed and you want the new content to be loaded.
Loading local content
Webviews run in isolated contexts that cannot directly access local resources. This is done for security reasons. This means that in order to load images, stylesheets, and other resources from your extension, or to load any content from the user's current workspace, you must use the Webview.asWebviewUri
function to convert a local file:
URI into a special URI that VS Code can use to load a subset of local resources.
Imagine that we want to start bundling the cat gifs into our extension rather than pulling them from Giphy. To do this, we first create a URI to the file on disk and then pass these URIs through the asWebviewUri
function:
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{}
);
// Get path to resource on disk
const onDiskPath = vscode.Uri.joinPath(context.extensionUri, 'media', 'cat.gif');
// And get the special URI to use with the webview
const catGifSrc = panel.webview.asWebviewUri(onDiskPath);
panel.webview.html = getWebviewContent(catGifSrc);
})
);
}
function getWebviewContent(catGifSrc: vscode.Uri) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<img src="${catGifSrc}" width="300" />
</body>
</html>`;
}
If we debug this code, we'd see that the actual value for catGifSrc
is something like:
vscode-resource:/Users/toonces/projects/vscode-cat-coding/media/cat.gif
VS Code understands this special URI and will use it to load our gif from the disk!
By default, webviews can only access resources in the following locations:
- Within your extension's install directory.
- Within the user's currently active workspace.
Use the WebviewOptions.localResourceRoots
to allow access to additional local resources.
You can also always use data URIs to embed resources directly within the webview.
Controlling access to local resources
Webviews can control which resources can be loaded from the user's machine with localResourceRoots
option. localResourceRoots
defines a set of root URIs from which local content may be loaded.
We can use localResourceRoots
to restrict Cat Coding webviews to only load resources from a media
directory in our extension:
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{
// Only allow the webview to access resources in our extension's media directory
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'media')],
}
);
const onDiskPath = vscode.Uri.joinPath(context.extensionUri, 'media', 'cat.gif');
const catGifSrc = panel.webview.asWebviewUri(onDiskPath);
panel.webview.html = getWebviewContent(catGifSrc);
})
);
}
To disallow all local resources, just set localResourceRoots
to []
.
In general, webviews should be as restrictive as possible in loading local resources. However, keep in mind that localResourceRoots
does not offer complete security protection on its own. Make sure your webview also follows security best practices, and add a content security policy to further restrict the content that can be loaded.