DEV Community

Cover image for Build a Custom VS Code Extension (Step-by-Step Guide for Beginners)
Anupam Kumar
Anupam Kumar

Posted on β€’ Edited on

Build a Custom VS Code Extension (Step-by-Step Guide for Beginners)

Why Build a VS Code Extension? 🌟

Visual Studio Code is more than an editorβ€”it's a playground for developers. With over 50,000 extensions on the Marketplace, the community loves customizing their experience. But have you ever thought, Hey, I could add that feature myself!? Today, we'll turn you from a consumer into a creator, showing you how to craft your own VS Code extension from scratchβ€”and dive into advanced features like TreeViews, Webviews, testing, CI, and more.

By the end of this guide, you’ll have:

  • A fully functional "Hello World" extension with actionable commands
  • A custom Tree View sidebar
  • A Webview panel for rich UIs
  • Automated unit & integration tests using Mocha
  • CI setup in GitHub Actions
  • Knowledge of packaging, publishing, and maintaining your extension

Ready to level up your dev game? Let’s dive in.


Prerequisites: Your Toolkit

Make sure you have:

  1. Node.js (v14+) – Your JavaScript runtime engine. Check with node -v.
  2. npm or Yarn – Package manager. npm -v or yarn -v.
  3. Visual Studio Code – Of course!
  4. Yeoman & VS Code Extension Generator – For scaffolding.
  5. vsce – VS Code Extension CLI for packaging and publishing.
# Install global dependencies
npm install -g yo generator-code vsce
# OR with yarn
yarn global add yo generator-code vsce
Enter fullscreen mode Exit fullscreen mode

Pro Tip: Use nvm to manage Node versions per project.


Step 1: Scaffold the Extension πŸ”§

Yeoman scaffolds boilerplate code:

yo code
Enter fullscreen mode Exit fullscreen mode

Prompts you'll see:

  • Type of extension: TypeScript (for safety and intellisense)
  • Extension name: my-first-extension
  • Identifier: myFirstExtension
  • Description: A brief summary
  • Initialize git?: Up to you!

Generated structure:

my-first-extension/
β”œβ”€β”€ .vscode/           # debug configurations
β”œβ”€β”€ src/               # TypeScript source
β”‚   └── extension.ts   # activation & commands
β”œβ”€β”€ package.json       # manifest & contributions
β”œβ”€β”€ tsconfig.json      # compile options
β”œβ”€β”€ README.md          # your docs
β”œβ”€β”€ .github/           # (if you choose CI)
└── test/              # Mocha tests
Enter fullscreen mode Exit fullscreen mode

Step 2: Deep Dive into package.json

Your extension’s manifest controls activation, commands, keybindings, views, configuration, and more.

{
  "name": "my-first-extension",
  "displayName": "My First Extension",
  "description": "Says hello, shows TreeView, and Webview!",
  "version": "0.0.1",
  "engines": { "vscode": "^1.60.0" },
  "activationEvents": [
    "onCommand:extension.sayHello",
    "onView:myTreeView"
  ],
  "main": "./out/extension.js",
  "contributes": {
    "commands": [
      { "command": "extension.sayHello", "title": "Hello World" }
    ],
    "keybindings": [
      {
        "command": "extension.sayHello",
        "key": "ctrl+alt+h",
        "when": "editorTextFocus"
      }
    ],
    "views": {
      "explorer": [
        { "id": "myTreeView", "name": "My Custom View" }
      ]
    },
    "configuration": {
      "type": "object",
      "properties": {
        "myExtension.showWelcome": {
          "type": "boolean",
          "default": true,
          "description": "Show welcome message on activation"
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  • activationEvents: Lazy-load your extension only when needed (commands, views, languages).
  • contributes.views: Register a TreeView in the Explorer panel.
  • keybindings: Let power users invoke commands with shortcuts.
  • configuration: Expose settings under VS Code’s Settings UI.

Step 3: Implement Core Features πŸ–‹οΈ

3.1 Say Hello Command

In src/extension.ts:

import * as vscode from 'vscode';
import { MyTreeDataProvider } from './treeProvider';
import { createWebviewPanel } from './webview';

export function activate(context: vscode.ExtensionContext) {
  const config = vscode.workspace.getConfiguration('myExtension');
  if (config.get('showWelcome')) {
    vscode.window.showInformationMessage('Welcome to My First Extension! 🌟');
  }

  // Hello Command
  const hello = vscode.commands.registerCommand('extension.sayHello', () => {
    vscode.window.showInformationMessage('Hello, VS Code Extension!', 'πŸŽ‰ Celebrate', 'πŸ“– Docs')
      .then(selection => {
        if (selection === 'πŸ“– Docs') {
          vscode.env.openExternal(vscode.Uri.parse('https://code.visualstudio.com/api'));
        }
      });
  });

  // TreeView
  const treeDataProvider = new MyTreeDataProvider();
  vscode.window.createTreeView('myTreeView', { treeDataProvider });

  // Webview
  const webviewCmd = vscode.commands.registerCommand('extension.showWebview', () => {
    createWebviewPanel(context);
  });

  context.subscriptions.push(hello, webviewCmd);
}

export function deactivate() {}
Enter fullscreen mode Exit fullscreen mode

3.2 Custom TreeView

Create src/treeProvider.ts:

import * as vscode from 'vscode';

export class MyTreeDataProvider implements vscode.TreeDataProvider<MyTreeItem> {
  private _onDidChange = new vscode.EventEmitter<MyTreeItem | void>();
  readonly onDidChangeTreeData = this._onDidChange.event;

  getChildren(): MyTreeItem[] {
    return [
      new MyTreeItem('Item One'),
      new MyTreeItem('Item Two')
    ];
  }

  getTreeItem(element: MyTreeItem): vscode.TreeItem {
    return element;
  }
}

class MyTreeItem extends vscode.TreeItem {
  constructor(label: string) {
    super(label);
    this.tooltip = `Tooltip for ${label}`;
    this.command = {
      command: 'extension.sayHello',
      title: 'Say Hello',
      arguments: []
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

3.3 Rich Webview Panel

Create src/webview.ts:

import * as vscode from 'vscode';

export function createWebviewPanel(context: vscode.ExtensionContext) {
  const panel = vscode.window.createWebviewPanel(
    'myWebview', 'My Webview', vscode.ViewColumn.One, { enableScripts: true }
  );

  panel.webview.html = getWebviewContent();
}

function getWebviewContent(): string {
  return `
    <!DOCTYPE html>
    <html lang="en">
    <head><meta charset="UTF-8" /><title>Webview</title></head>
    <body>
      <h1>Hello from Webview!</h1>
      <button onclick="sendMessage()">Click me</button>
      <script>
        const vscode = acquireVsCodeApi();
        function sendMessage() {
          vscode.postMessage({ command: 'alert', text: 'Button clicked!' });
        }
      </script>
    </body>
    </html>
  `;
}
Enter fullscreen mode Exit fullscreen mode

Add message listener in activate if needed.


Step 4: Compile, Debug & Lint 🚦

  1. Compile: npm run compile
  2. Lint: Add ESLint (npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin)
  3. Debug: Press F5. In the Extension Development Host:
    • Command Palette: Ctrl+Shift+P β†’ Hello World
    • Explorer: Find My Custom View
    • Command Palette: Show Webview

Step 5: Testing πŸ§ͺ

Yeoman scaffold includes Mocha. In test\extension.test.ts:

import * as assert from 'assert';
import * as vscode from 'vscode';

describe('Extension Tests', () => {
  it('should activate extension', async () => {
    const ext = vscode.extensions.getExtension('your-publisher.my-first-extension');
    await ext?.activate();
    assert.ok(ext?.isActive);
  });
});
Enter fullscreen mode Exit fullscreen mode

Run tests:

npm test
Enter fullscreen mode Exit fullscreen mode

Consider adding integration tests with @vscode/test-electron for UI flows.


Step 6: Continuous Integration πŸ€–

Use GitHub Actions. Example .github/workflows/ci.yml:

name: CI
on: [push, pull_request]
jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with: { 'node-version': '14' }
      - run: npm install
      - run: npm run compile
      - run: npm test
      - run: npm run lint
Enter fullscreen mode Exit fullscreen mode

This ensures every PR builds, compiles, and tests cleanly.


Step 7: Package & Publish

  1. Package: vsce package β†’ produces my-first-extension-0.0.1.vsix
  2. Publish:
   vsce login <publisher-name>
   vsce publish
Enter fullscreen mode Exit fullscreen mode
  1. Versioning: Follow Semantic Versioning to keep users happy.

Pro Tips & Best Practices

  • Lazy Activation: Only load heavy modules on demand.
  • Telemetry: Use vscode-extension-telemetry; always ask permission and respect GDPR.
  • Localization: Support multiple languages with package.nls.json.
  • Performance: Avoid blocking the main thread. Use background tasks if needed.
  • Documentation: Include a clear README, CHANGELOG, and demo GIFs.
  • Community: Respond to issues, tag PRs, and keep dependencies updated.

Wrapping Up

You’ve built commands, views, rich web UIs, added testing, CI, and deployed to the Marketplace. But this is just the beginning:

  • Explore Debug Adapters and Language Servers
  • Create Custom Themes and Syntax Grammars
  • Integrate AI/ML for smarter coding assistance

Drop your extension links in the comments, share your learnings, and let’s push the boundaries of what VS Code can doβ€”together. Happy coding!


Enjoyed this deep dive? Follow me for more tutorials and code adventures!

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.