| name | lit-and-vscode-extensions |
| description | Learn how to build a VSCode extension using a Lit web component, covering setup, template creation, component implementation, and extension activation. |
| metadata | {"url":"https://rodydavis.com/posts/lit-vscode-extension","last_modified":"Tue, 03 Feb 2026 20:04:18 GMT"} |
Lit and VSCode Extensions
In this article I will go over how to set up aĀ Lit web component and use it to create a VSCode extension.
TLDRĀ You can find the final sourceĀ here.
PrerequisitesĀ
- Vscode
- Node >= 16
- Typescript
Getting StartedĀ
We can start off by navigating in terminal to the location of the project and run the following:
npm init @vitejs/app --template lit-ts
Then enter a project nameĀ lit-vscode-extension and now open the project in vscode and install the dependencies:
cd lit-vscode-extension
npm i -D @types/node
code .
Update theĀ vite.config.ts with the following:
import { defineConfig } from "vite";
import { resolve } from "path";
export default defineConfig({
base: "/lit-vscode-extension/",
build: {
outDir: "build",
rollupOptions: {
external: /^vscode/,
input: {
main: resolve(__dirname, "index.html"),
},
output: {
entryFileNames: "[name].js",
},
},
},
});
OpenĀ package.json and update it with the following:
{
"name": "lit-vscode-extension",
"description": "Lit VSCode Extension Example",
"version": "0.0.1",
"publisher": "rodydavis",
"private": true,
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/rodydavis/lit-vscode-extension"
},
"engines": {
"vscode": "^1.47.0"
},
"categories": [
"Other"
],
"activationEvents": [
"onCommand:lit.start",
"onCommand:lit.reset",
"onWebviewPanel:lit"
],
"main": "./build/extension.js",
"contributes": {
"commands": [
{
"command": "lit.start",
"title": "Open Plugin",
"category": "lit"
},
{
"command": "lit.reset",
"title": "Reset",
"category": "lit"
}
]
},
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"ext": "tsc src/extension.ts --outdir build --skipLibCheck --module commonjs",
"compile": "npm run build && npm run ext",
"serve": "vite preview"
},
"dependencies": {
"lit": "^2.0.0-rc.2"
},
"devDependencies": {
"@types/node": "^15.12.4",
"@types/vscode": "^1.57.0",
"typescript": "^4.2.3",
"vite": "^2.3.5"
}
}
After theĀ package.jsonĀ is updated runĀ npm i.
TemplateĀ
Open up theĀ index.html and update it with the following:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lit Example</title>
<script type="module" src="/src/my-element.ts"></script>
</head>
<body>
<my-element>
<p>This is child content</p>
</my-element>
</body>
</html>
Web ComponentĀ
Open upĀ src/my-element.tsĀ and update it with the following:
import { html, css, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators.js";
@customElement("my-element")
export class MyElement extends LitElement {
static styles = css`
:host {
display: block;
border: solid 1px gray;
padding: 16px;
max-width: 800px;
}
`;
@property() name = "World";
@state() count = 0;
render() {
return html`
<h1>Hello, ${this.name}!</h1>
<button @click=${() => this.modify(1)} part="button">
Click Count: ${this.count}
</button>
<slot></slot>
`;
}
modify(val: number) {
this.count += val;
}
reset() {
this.count = 0;
}
async firstUpdated() {
window.addEventListener(
"message",
(e: any) => {
const message = e.data;
const { command } = message;
if (command === "reset") {
this.reset();
}
},
false
);
}
}
Here we are just modifying the example to include a message listener for communicating with the vscode extension, methods for updating the count, and updating the render method.
VSCode communicates with the plugin via post messages because the UI will be loaded in anĀ iframe.
Now let's write the extension code inĀ src/extension.ts. First start by adding top level declarations that will be referenced multiple times.
import * as vscode from "vscode";
const WEB_DIR: string = "build";
const WEB_SCRIPT: string = "main.js";
const TITLE: string = "Lit Example";
const TAG: string = "my-element";
const possible = [
"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
"abcdefghijklmnopqrstuvwxyz",
"0123456789",
].join("");
function getNonce() {
let text = "";
for (let i = 0; i < 32; i++) {
const char = possible.charAt(Math.floor(Math.random() * possible.length));
text += char;
}
return text;
}
We also are creating aĀ getNonceĀ method that will be used for making sure the scripts we load are the ones we passed in.
Now create aĀ Panel that will contain the ui for our plugin:
class Panel {
public static currentPanel: Panel | undefined;
public static readonly viewType = "litExample";
private _disposables: vscode.Disposable[] = [];
public static createOrShow(extensionUri: vscode.Uri) {
const column = vscode.window.activeTextEditor
? vscode.window.activeTextEditor.viewColumn
: undefined;
if (Panel.currentPanel) {
Panel.currentPanel.panel.reveal(column);
return;
}
const panel = vscode.window.createWebviewPanel(
Panel.viewType,
TITLE,
column || vscode.ViewColumn.One,
getWebviewOptions(extensionUri)
);
Panel.currentPanel = new Panel(panel, extensionUri);
}
public static revive(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
Panel.currentPanel = new Panel(panel, extensionUri);
}
private constructor(
public readonly panel: vscode.WebviewPanel,
public readonly extensionUri: vscode.Uri
) {
this._update();
this.panel.onDidDispose(() => this.dispose(), null, this._disposables);
this.panel.onDidChangeViewState(
(_) => {
if (this.panel.visible) {
this._update();
}
},
null,
this._disposables
);
this.panel.webview.onDidReceiveMessage(
(message) => {
switch (message.command) {
case "alert":
vscode.window.showErrorMessage(message.text);
return;
}
},
null,
this._disposables
);
}
public sendMessage(command: string) {
this.panel.webview.postMessage({ command: command });
}
public dispose() {
Panel.currentPanel = undefined;
this.panel.dispose();
while (this._disposables.length) {
const x = this._disposables.pop();
if (x) {
x.dispose();
}
}
}
private _update() {
const webview = this.panel.webview;
webview.html = this._getHtmlForWebview(webview);
}
private _getHtmlForWebview(webview: vscode.Webview) {
const scriptPathOnDisk = vscode.Uri.joinPath(
this.extensionUri,
WEB_DIR,
WEB_SCRIPT
);
const scriptUri = webview.asWebviewUri(scriptPathOnDisk);
const nonce = getNonce();
const slot = "<p>This is child content</p>";
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${TITLE}</title>
</head>
<body class="vscode-light">
<${TAG} nonce="${nonce}" >
${slot}
</${TAG}>
<script nonce="${nonce}" type="module" src="${scriptUri}"></script>
</body>
</html>`;
}
}
Notice how we are recreating the html and not using theĀ index.html. This allows us to use the component for a deployed website but also the extension with separate app logic code.
Everything else is just boilerplate for managing the panel state and disposing when it is finished.
Now we can add the methods for loading our plugin and listening for the commands:
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("lit.start", () => {
Panel.createOrShow(context.extensionUri);
})
);
context.subscriptions.push(
vscode.commands.registerCommand("lit.reset", () => {
if (Panel.currentPanel) {
Panel.currentPanel.sendMessage("reset");
}
})
);
if (vscode.window.registerWebviewPanelSerializer) {
vscode.window.registerWebviewPanelSerializer(Panel.viewType, {
async deserializeWebviewPanel(
webviewPanel: vscode.WebviewPanel,
state: any
) {
console.log(`Received state: ${state}`);
webviewPanel.webview.options = getWebviewOptions(context.extensionUri);
Panel.revive(webviewPanel, context.extensionUri);
},
});
}
}
function getWebviewOptions(extensionUri: vscode.Uri): vscode.WebviewOptions {
return {
enableScripts: true,
localResourceRoots: [vscode.Uri.joinPath(extensionUri, WEB_DIR)],
};
}
Here we are loading the extension and passing messages when theĀ lit.resetĀ command. We are also returning theĀ getWebviewOptionsĀ options whichĀ enableScriptsĀ is set toĀ trueĀ so we can run the injected js.
The final code should look like the follow:
import * as vscode from "vscode";
const WEB_DIR: string = "build";
const WEB_SCRIPT: string = "main.js";
const TITLE: string = "Lit Example";
const TAG: string = "my-element";
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("lit.start", () => {
Panel.createOrShow(context.extensionUri);
})
);
context.subscriptions.push(
vscode.commands.registerCommand("lit.reset", () => {
if (Panel.currentPanel) {
Panel.currentPanel.sendMessage("reset");
}
})
);
if (vscode.window.registerWebviewPanelSerializer) {
vscode.window.registerWebviewPanelSerializer(Panel.viewType, {
async deserializeWebviewPanel(
webviewPanel: vscode.WebviewPanel,
state: any
) {
console.log(`Received state: ${state}`);
webviewPanel.webview.options = getWebviewOptions(context.extensionUri);
Panel.revive(webviewPanel, context.extensionUri);
},
});
}
}
function getWebviewOptions(extensionUri: vscode.Uri): vscode.WebviewOptions {
return {
enableScripts: true,
localResourceRoots: [vscode.Uri.joinPath(extensionUri, WEB_DIR)],
};
}
class Panel {
public static currentPanel: Panel | undefined;
public static readonly viewType = "litExample";
private _disposables: vscode.Disposable[] = [];
public static createOrShow(extensionUri: vscode.Uri) {
const column = vscode.window.activeTextEditor
? vscode.window.activeTextEditor.viewColumn
: undefined;
if (Panel.currentPanel) {
Panel.currentPanel.panel.reveal(column);
return;
}
const panel = vscode.window.createWebviewPanel(
Panel.viewType,
TITLE,
column || vscode.ViewColumn.One,
getWebviewOptions(extensionUri)
);
Panel.currentPanel = new Panel(panel, extensionUri);
}
public static revive(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
Panel.currentPanel = new Panel(panel, extensionUri);
}
private constructor(
public readonly panel: vscode.WebviewPanel,
public readonly extensionUri: vscode.Uri
) {
this._update();
this.panel.onDidDispose(() => this.dispose(), null, this._disposables);
this.panel.onDidChangeViewState(
(_) => {
if (this.panel.visible) {
this._update();
}
},
null,
this._disposables
);
this.panel.webview.onDidReceiveMessage(
(message) => {
switch (message.command) {
case "alert":
vscode.window.showErrorMessage(message.text);
return;
}
},
null,
this._disposables
);
}
public sendMessage(command: string) {
this.panel.webview.postMessage({ command: command });
}
public dispose() {
Panel.currentPanel = undefined;
this.panel.dispose();
while (this._disposables.length) {
const x = this._disposables.pop();
if (x) {
x.dispose();
}
}
}
private _update() {
const webview = this.panel.webview;
webview.html = this._getHtmlForWebview(webview);
}
private _getHtmlForWebview(webview: vscode.Webview) {
const scriptPathOnDisk = vscode.Uri.joinPath(
this.extensionUri,
WEB_DIR,
WEB_SCRIPT
);
const scriptUri = webview.asWebviewUri(scriptPathOnDisk);
const nonce = getNonce();
const slot = "<p>This is child content</p>";
const htmlSource = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${TITLE}</title>
</head>
<body class="vscode-light">
<${TAG} nonce="${nonce}" >
${slot}
</${TAG}>
<script nonce="${nonce}" type="module" src="${scriptUri}"></script>
</body>
</html>`;
return htmlSource;
}
}
const possible = [
"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
"abcdefghijklmnopqrstuvwxyz",
"0123456789",
].join("");
function getNonce() {
let text = "";
for (let i = 0; i < 32; i++) {
const char = possible.charAt(Math.floor(Math.random() * possible.length));
text += char;
}
return text;
}
RunningĀ
Make sure to install the dependencies by runningĀ npm i.
To build the extension runĀ npm run compile.
To open the extension and debug hitĀ F5.

To run the commands to open the extension runĀ lit: open pluginĀ orĀ lit: reset:

To debug the extension when it is open runĀ Developer: Open Webview Developer Tools from the command pallet.

ConclusionĀ
If you want to learn more about building a vscode extension you can read moreĀ hereĀ and for Lit you can read the docsĀ here.