ABDULKADERSAFI.COM
Back to Blog
Tailwindcss Typescript VS Code Extension React Svelte

Open-Source VS Code Extension Starters: React and Svelte with Tailwind

6 min read

Two open-source VS Code extension starter kits — one React 19, one Svelte 5 — both wired with TailwindCSS v4, TypeScript, esbuild, routing, and working webview↔extension messaging. Clone, rename, ship.

Split screen showing a VS Code extension webview built with React on one side and Svelte on the other, both styled with TailwindCSS
Table of Contents

Setting up a VS Code extension with a modern webview is annoying.

Not hard — annoying. You have to stitch together esbuild, PostCSS, Tailwind, TypeScript, a routing layer, a message bridge between the extension host and the webview, Content Security Policy nonces, acquireVsCodeApi() typings, the VS Code contribution points in package.json… and the official yo code generator still gives you a vanilla TypeScript file with showInformationMessage('Hello World') as the only example. Useful in 2017. Not in 2026.

So I open-sourced the two templates I actually use when I start a new extension — one with React, one with Svelte — both fully wired and identical on the extension side.

Here's what you get:

  • github.com/Abdulkader-Safi/vs-code-extension-react-starter — React 19, React Router DOM 7, TailwindCSS v4, esbuild, TypeScript.
  • github.com/Abdulkader-Safi/vs-code-extension-svelte-starter — Svelte 5 (runes), svelte-spa-router, TailwindCSS v4, esbuild, TypeScript.

Both ship two working demo pages: a notification page that pushes messages from the webview into VS Code, and a directory listing page that pulls workspace data back the other way. That covers the two directions of webview messaging most extensions actually need.

Clone, rename, start building. That's the pitch.

Why Two Starters (and Not Just One)

When I was building ClarifAI, I started with React because that's what every VS Code webview tutorial uses. React is fine. It works. But the final packaged extension was heavier than I wanted for what was essentially a side panel.

Then I built a second extension where the UI was even smaller — a settings form, basically — and I tried Svelte. Bundle dropped by roughly 60%. Cold start felt snappier. And for a webview that lives inside someone's editor, cold start matters more than it does on a website.

So the real answer to "React or Svelte?" depends on what you're building:

  • React 19 → you already ship React, you need a specific React-only component (think data grids, PDF viewers, rich editors), or the webview will grow into a small SPA.
  • Svelte 5 → the UI is small-to-medium, you care about bundle size, or you want the least JavaScript shipped per panel load.

Both templates exist so you don't have to pick at setup time — you can clone either and the extension-host code is interchangeable.

What's Inside Each Starter

I'll walk through the React one first. The Svelte one is structurally identical — different framework, same scaffolding.

The Project Structure

src/
├── extension.ts           # VS Code extension entry point
├── WebviewProvider.ts     # Webview lifecycle + HTML + CSP
└── webview/
    ├── main.tsx           # React root
    ├── App.tsx            # Routing
    ├── pages/
    │   ├── Notification.tsx
    │   └── DirectoryListing.tsx
    └── styles/index.css   # Tailwind entry
dist/                      # esbuild output — this is what ships
esbuild.js                 # Single bundler config for both sides
tailwind.config.js
postcss.config.js
package.json

Two things to notice:

  1. One bundler, two builds. esbuild.js produces both the extension-side JS and the webview-side JS in a single run. No split webpack configs. No Vite-for-webview-but-tsc-for-extension. Just esbuild.
  2. Clean boundary. Everything under src/webview/ is the UI. Everything outside is the VS Code extension. They talk only via postMessage. This is the boundary that prevents a whole category of "why did importing this break my activation?" bugs.

The Extension Side

extension.ts is tiny — it registers two commands and launches the webview:

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('react-starter.helloWorld', () => {
      vscode.window.showInformationMessage('Hello from React Starter');
    }),
    vscode.commands.registerCommand('react-starter.openReactView', () => {
      WebviewProvider.render(context);
    })
  );
}

WebviewProvider.ts is where the actual work happens. It creates the webview panel, loads the compiled JS/CSS from dist/, injects a Content Security Policy with a nonce (the thing most tutorials skip and then wonder why scripts don't execute), and wires up the message listener:

panel.webview.onDidReceiveMessage(async (message) => {
  switch (message.type) {
    case 'show-notification':
      vscode.window.showInformationMessage(message.text);
      return;
    case 'list-files':
      const files = await vscode.workspace.findFiles('**/*', '**/node_modules/**');
      panel.webview.postMessage({ type: 'files-result', files });
      return;
  }
});

That pattern — discriminated-union messages by type — scales all the way up. ClarifAI uses the same pattern with 14 message types and it's still readable.

The Webview Side

This is the part that's different between the two templates. In the React version, main.tsx mounts App, App.tsx defines routes with React Router, and each page calls vscode.postMessage() and listens on window for responses.

The trick I ended up bundling into both templates is a tiny hook (or Svelte action, on the other side) that wraps the message-listener lifecycle:

// useVSCodeMessage.ts
export function useVSCodeMessage<T>(type: string, handler: (data: T) => void) {
  useEffect(() => {
    const listener = (event: MessageEvent) => {
      if (event.data?.type === type) handler(event.data);
    };
    window.addEventListener('message', listener);
    return () => window.removeEventListener('message', listener);
  }, [type, handler]);
}

Ten lines. Never write that boilerplate again.

The Svelte equivalent is even shorter because runes handle the cleanup implicitly via $effect.

TailwindCSS v4 Inside a Webview (The Non-Obvious Part)

Tailwind v4 moved to a PostCSS-first architecture, which sounds like a small thing until you realize the old tailwindcss init + content: [] config doesn't exist anymore. For a VS Code webview, you want:

  1. @tailwindcss/postcss as a PostCSS plugin.
  2. Your CSS entry file imports @import "tailwindcss"; — that's it.
  3. esbuild runs PostCSS as part of the build via a plugin.

Both starters ship this configured. If you clone, npm install, npm run compile, and press F5, the webview opens with Tailwind working. No extra setup.

This is also where Svelte gets a quiet win — Svelte 5's scoped CSS composes cleanly with Tailwind utilities, so you can use class: directives and utility classes together without the specificity wars you sometimes hit in a React app.

Running It Locally (Three Steps)

After cloning either repo:

npm install
npm run watch

That starts esbuild in watch mode for both the extension and the webview. Then in VS Code, press F5 to open the Extension Development Host — a second VS Code window where your extension is loaded. In that window, open the Command Palette (Cmd+Shift+P / Ctrl+Shift+P) and run "Open React View" or "Open Svelte View" depending on which template you cloned.

The webview opens. The demo pages work. You've got a running extension in under 60 seconds from git clone.

When you're ready to ship, npm run package produces a production build, and vsce package turns it into a .vsix you can publish to the marketplace. I covered the publishing flow in detail in Building VS Code Extensions in 2026.

What's Deliberately Missing

A few things I left out on purpose, because every extension's needs differ:

  • State management. Both templates use local component state. If your extension grows into a real SPA, drop in whatever you already like. For Svelte, I've written up why Zustand can make sense even there — but for most extensions, writable stores or React's useState is enough.
  • Theme syncing. VS Code's light/dark/high-contrast themes expose CSS variables to webviews. Tailwind v4 makes this easy to consume, but the exact mapping depends on your design, so the templates leave it unopinionated.
  • Authentication, API keys, secrets. Use VS Code's SecretStorage API, not globalState. That's extension-specific — I wasn't going to hard-code a pattern.
  • Tests beyond the scaffold. @vscode/test-cli is wired up and runs a trivial sanity test. Real testing is a per-project decision.

The goal is a clean starting point, not a kitchen sink. I'd rather you delete three files than hunt through fifteen layers of abstraction to find the one thing you actually want to change.

When to Use Which Starter (Honest Take)

Here's the heuristic I actually use:

Situation Pick
Webview is a small-to-medium form or panel Svelte
You need a specific React library (react-flow, react-pdf, ag-grid) React
Your team ships React across all products React
Bundle size under 100KB matters Svelte
You want to learn Svelte 5 runes in a real project Svelte
You'll hand off to a team of 10+ devs Whichever they already know

Both ship today. Both will keep working. And because the extension-host layer is identical, I can port a project between them in about two hours if I guess wrong.

What to Do Now

  1. Clone the one that fits your project:
    • React: git clone https://github.com/Abdulkader-Safi/vs-code-extension-react-starter
    • Svelte: git clone https://github.com/Abdulkader-Safi/vs-code-extension-svelte-starter
  2. Rename the extension. Update name, displayName, and the command IDs in package.json. Five minutes.
  3. Delete the two demo pages once you understand them — they're there to show the messaging pattern, not to ship.
  4. Open an issue on either repo if something's broken or missing. I use these templates every time I start a new extension, so PRs that genuinely improve them land fast.

If you want more context on the bigger picture of extension development — marketplace publishing, activation events, the contribution API — my longer guide Building VS Code Extensions in 2026 picks up where these starters leave off. And if you want to see two extensions I shipped using this exact stack, check out ClarifAI and the file explorer I built because macOS Finder is painful.

Star the repos if they save you an afternoon. That's the only payment I'll ask for

FAQ

Frequently Asked Questions

What is the easiest way to start a VS Code extension with a modern webview?

The fastest path is to clone a fully wired starter rather than using the official yo code generator, which still produces a vanilla TypeScript Hello World example. These two open-source starters give you esbuild, PostCSS, TailwindCSS v4, TypeScript, routing, a webview-to-extension message bridge, and Content Security Policy nonces already configured. You clone, rename the extension in package.json, and start building, with a running extension in under 60 seconds from git clone.

Should you build a VS Code extension webview with React or Svelte?

It depends on what you are building. Choose React 19 if you already ship React, need a specific React-only library like a data grid or PDF viewer, or expect the webview to grow into a small single-page app. Choose Svelte 5 if the UI is small to medium, you care about bundle size, or you want the least JavaScript shipped per panel load. In one real project, switching the same small UI from React to Svelte dropped the bundle by roughly 60% and improved cold start, which matters more inside an editor than on a website.

How does messaging work between a VS Code webview and the extension host?

The webview and extension communicate only through postMessage, which keeps a clean boundary and avoids a whole class of activation bugs. The webview calls vscode.postMessage to send data, and the extension host listens with panel.webview.onDidReceiveMessage and switches on a message type field, replying back with panel.webview.postMessage. This discriminated-union pattern keyed by message type scales well, and both starters ship a small hook or Svelte action that wraps the message-listener lifecycle so you never rewrite that boilerplate.

How do you set up TailwindCSS v4 inside a VS Code webview?

Tailwind v4 moved to a PostCSS-first architecture, so the old init command and content array config no longer exist. You add the tailwindcss PostCSS plugin, import tailwindcss in your CSS entry file with a single import line, and have esbuild run PostCSS as part of the build through a plugin. Both starters ship this preconfigured, so after cloning you just install dependencies, compile, and press F5 to see Tailwind working in the webview with no extra setup.

How do you run and test a VS Code extension starter locally?

After cloning either repo, run npm install and then npm run watch, which starts esbuild in watch mode for both the extension and the webview. Press F5 in VS Code to open the Extension Development Host, a second window where your extension is loaded, then open the Command Palette and run Open React View or Open Svelte View depending on the template. When you are ready to ship, npm run package produces a production build and vsce package turns it into a .vsix you can publish to the marketplace.

GET IN TOUCH

Let's Build Something Together

Have a project in mind, want to collaborate on a web or mobile app, or just want to say hi? My inbox is open.

Get in Touch
safi.abdulkader@gmail.com +965 60787763 Based in Kuwait & Lebanon