惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News and Events Feed by Topic
酷 壳 – CoolShell
酷 壳 – CoolShell
G
Google Developers Blog
N
Netflix TechBlog - Medium
Recorded Future
Recorded Future
S
Securelist
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Scott Helme
Scott Helme
C
Check Point Blog
量子位
月光博客
月光博客
Last Week in AI
Last Week in AI
C
Cisco Blogs
G
GRAHAM CLULEY
B
Blog RSS Feed
K
Kaspersky official blog
爱范儿
爱范儿
J
Java Code Geeks
I
Intezer
aimingoo的专栏
aimingoo的专栏
WordPress大学
WordPress大学
云风的 BLOG
云风的 BLOG
C
CXSECURITY Database RSS Feed - CXSecurity.com
H
Hackread – Cybersecurity News, Data Breaches, AI and More
博客园 - 三生石上(FineUI控件)
罗磊的独立博客
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
MongoDB | Blog
MongoDB | Blog
T
Threat Research - Cisco Blogs
N
News | PayPal Newsroom
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Security Latest
Security Latest
T
Tailwind CSS Blog
人人都是产品经理
人人都是产品经理
P
Privacy International News Feed
The Cloudflare Blog
H
Heimdal Security Blog
宝玉的分享
宝玉的分享
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
腾讯CDC
GbyAI
GbyAI
V
Visual Studio Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
博客园 - 聂微东
T
Tenable Blog
The Register - Security
The Register - Security
AI
AI
C
Cybersecurity and Infrastructure Security Agency CISA
S
Schneier on Security
L
LangChain Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Build a VS Code Webview with Vue, Vite, Tailwind CSS, and VS Code-Themed Components
Moya Richard · 2026-05-13 · via DEV Community

This tutorial shows how to create a VS Code extension that opens a custom webview powered by:

  • Vue for the webview UI
  • Vite for fast frontend builds
  • Tailwind CSS for layout and custom styling
  • VS Code CSS theme variables for native-looking controls
  • VS Code Webview API for rendering the UI inside VS Code

1. Prerequisites

Install Node.js and npm first.

Then install the VS Code extension generator:

npm install --global yo generator-code

Enter fullscreen mode Exit fullscreen mode

You should also have VS Code installed.


2. Create the VS Code Extension

Run:

yo code

Enter fullscreen mode Exit fullscreen mode

Choose these options:

✔ What type of extension do you want to create? New Extension (TypeScript)
✔ What's the name of your extension? vue-webview-vite
✔ What's the identifier of your extension? vue-webview-vite
✔ What's the description of your extension? webview
✔ Initialize a git repository? Yes
✔ Which bundler to use? unbundled
✔ Which package manager to use? npm

Enter fullscreen mode Exit fullscreen mode

This creates the extension shell where the important files are:

vue-webview-vite/
  package.json
  src/
    extension.ts
  tsconfig.json

Enter fullscreen mode Exit fullscreen mode

Update the extension root tsconfig.json so the extension compiler only reads files from src/ and does not accidentally compile the Vite Vue app inside webview-ui.

{
  "compilerOptions": {
    "module": "Node16",
    "target": "ES2022",
    "outDir": "out",
    "lib": ["ES2022"],
    "sourceMap": true,
    "rootDir": "src",
    "strict": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules", "webview-ui", "out"]
}

Enter fullscreen mode Exit fullscreen mode

The important part is:

"include": ["src/**/*.ts"],
"exclude": ["node_modules", "webview-ui", "out"]

Enter fullscreen mode Exit fullscreen mode

Without include, TypeScript may use the default pattern **/*, which can accidentally compile the Vite Vue app inside webview-ui.


3. Run the Extension for the First Time

Open the generated extension in VS Code and click on src/extension.ts.

Press:

F5

Enter fullscreen mode Exit fullscreen mode

This starts the extension in a new window called the Extension Development Host.

In that new window:

  1. Open the Command Palette:
  • Windows: Ctrl + Shift + P
  • macOS: Cmd + Shift + P
    1. Run:
Hello World

Enter fullscreen mode Exit fullscreen mode

You should see the default extension message:

Hello World from vue-webview-vite!

Enter fullscreen mode Exit fullscreen mode

This confirms that the extension host works before adding Vue.


4. Add a Vue App with Vite

From the extension root, run the command below to create a Vue + TypeScript app inside a folder named webview-ui:

npm create vite@latest webview-ui

Enter fullscreen mode Exit fullscreen mode

Choose these options:

✔ Select a framework? Vue
✔ Select a variant? TypeScript

Enter fullscreen mode Exit fullscreen mode

Install the webview app dependencies:

cd webview-ui
npm install

Enter fullscreen mode Exit fullscreen mode

Your project should now look like this:

vue-webview-vite/
  src/
    extension.ts
  webview-ui/
    index.html
    package.json
    src/
      App.vue
      main.ts
      style.css
    vite.config.ts

Enter fullscreen mode Exit fullscreen mode


5. Install Tailwind CSS for Vite

Make sure you are inside webview-ui:

cd webview-ui

Enter fullscreen mode Exit fullscreen mode

Install Tailwind using the Vite plugin:

npm install tailwindcss @tailwindcss/vite

Enter fullscreen mode Exit fullscreen mode

Update webview-ui/vite.config.ts:

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [vue(), tailwindcss()],
  build: {
    outDir: "dist",
    emptyOutDir: true,
    rollupOptions: {
      output: {
        entryFileNames: "assets/[name].js",
        chunkFileNames: "assets/[name].js",
        assetFileNames: "assets/[name].[ext]",
      },
    },
  },
});

Enter fullscreen mode Exit fullscreen mode

Update webview-ui/src/style.css:

@import "tailwindcss";

:root {
  font-family: var(--vscode-font-family);
  color: var(--vscode-foreground);
  background: var(--vscode-editor-background);
}

body {
  margin: 0;
  min-width: 320px;
  min-height: 100vh;
  background: var(--vscode-editor-background);
  color: var(--vscode-foreground);
}

button,
input,
textarea,
select {
  font-family: inherit;
}

Enter fullscreen mode Exit fullscreen mode

This setup keeps Tailwind available while still respecting VS Code theme variables.


6. Create VS Code-Themed Vue Components

  • Build regular Vue components.
  • Style them with Tailwind.
  • Use VS Code CSS variables such as --vscode-button-background, --vscode-input-background, and --vscode-panel-border.

Create webview-ui/src/components/VSCodeButton.vue:

<script setup lang="ts">
defineProps<{
  variant?: "primary" | "secondary";
}>();
</script>

<template>
  <button
    :class="[
      'rounded px-3 py-1.5 text-sm font-medium focus:outline focus:outline-1 focus:outline-[var(--vscode-focusBorder)]',
      variant === 'secondary'
        ? 'bg-[var(--vscode-button-secondaryBackground)] text-[var(--vscode-button-secondaryForeground)] hover:bg-[var(--vscode-button-secondaryHoverBackground)]'
        : 'bg-[var(--vscode-button-background)] text-[var(--vscode-button-foreground)] hover:bg-[var(--vscode-button-hoverBackground)]',
    ]"
  >
    <slot />
  </button>
</template>

Enter fullscreen mode Exit fullscreen mode

Create webview-ui/src/components/VSCodeTextField.vue:

<script setup lang="ts">
defineProps<{
  modelValue: string;
  placeholder?: string;
}>();

defineEmits<{
  "update:modelValue": [value: string];
}>();
</script>

<template>
  <input
    :value="modelValue"
    :placeholder="placeholder"
    class="w-full rounded border border-[var(--vscode-input-border)] bg-[var(--vscode-input-background)] px-3 py-2 text-sm text-[var(--vscode-input-foreground)] placeholder:text-[var(--vscode-input-placeholderForeground)] focus:outline focus:outline-1 focus:outline-[var(--vscode-focusBorder)]"
    @input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
  />
</template>

Enter fullscreen mode Exit fullscreen mode

Create webview-ui/src/components/VSCodeTextArea.vue:

<script setup lang="ts">
defineProps<{
  modelValue: string;
  placeholder?: string;
}>();

defineEmits<{
  "update:modelValue": [value: string];
}>();
</script>

<template>
  <textarea
    :value="modelValue"
    :placeholder="placeholder"
    class="min-h-24 w-full rounded border border-[var(--vscode-input-border)] bg-[var(--vscode-input-background)] px-3 py-2 text-sm text-[var(--vscode-input-foreground)] placeholder:text-[var(--vscode-input-placeholderForeground)] focus:outline focus:outline-1 focus:outline-[var(--vscode-focusBorder)]"
    @input="$emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
  />
</template>

Enter fullscreen mode Exit fullscreen mode

Create webview-ui/src/components/VSCodeCard.vue:

<template>
  <section
    class="rounded-xl border border-[var(--vscode-panel-border)] bg-[var(--vscode-sideBar-background)] p-5 shadow-sm"
  >
    <slot />
  </section>
</template>

Enter fullscreen mode Exit fullscreen mode

The UI combines Tailwind utility classes with VS Code CSS variables such as:

var(--vscode-editor-background)
var(--vscode-foreground)
var(--vscode-descriptionForeground)
var(--vscode-panel-border)
var(--vscode-button-background)
var(--vscode-input-background)

Enter fullscreen mode Exit fullscreen mode

That keeps the webview aligned with the active VS Code theme.


7. Create the Vue Webview UI with VS Code API Messaging

Create the Vue webview UI with VS Code message passing included from the start.

A webview runs like a small web app, but it cannot directly call the VS Code extension API. Instead, the Vue app sends messages to the extension, and the extension runs VS Code APIs on its behalf.

The flow looks like this:

Vue button click
  → webview postMessage
  → extension receives message
  → extension runs VS Code API
  → extension sends response back to Vue
  → Vue updates the UI

Enter fullscreen mode Exit fullscreen mode

Create the VS Code API wrapper

Create webview-ui/src/api/vscode-api.ts:

export type WebviewMessage =
  | {
      type: "saveSettings";
      payload: {
        projectName: string;
        format: string;
        notes: string;
      };
    }
  | {
      type: "showInfo";
      payload: {
        message: string;
      };
    };

let vscodeApi: VSCodeApi | undefined;

function getVsCodeApi() {
  if (vscodeApi) {
    return vscodeApi;
  }

  if (typeof acquireVsCodeApi === "function") {
    vscodeApi = acquireVsCodeApi();
    return vscodeApi;
  }

  return undefined;
}

export function postMessage(message: WebviewMessage) {
  const vscode = getVsCodeApi();

  if (!vscode) {
    console.log("VS Code API is not available. Message was not sent:", message);
    return;
  }

  vscode.postMessage(message);
}

export function getVsCodeState<T>() {
  const vscode = getVsCodeApi();
  return vscode?.getState<T>();
}

export function setVsCodeState<T>(state: T) {
  const vscode = getVsCodeApi();
  vscode?.setState(state);
}

Enter fullscreen mode Exit fullscreen mode

This wrapper is the only place where the Vue app should call acquireVsCodeApi().

The wrapper calls acquireVsCodeApi() lazily, only when the app needs to communicate with VS Code. This prevents the app from crashing during Vite browser development or testing.

Add webview API types for the wrapper

Create webview-ui/src/vscode.d.ts:

type VSCodeApi = {
  postMessage: (message: unknown) => void;
  getState: <T = unknown>() => T | undefined;
  setState: <T = unknown>(state: T) => void;
};

declare function acquireVsCodeApi(): VSCodeApi;

Enter fullscreen mode Exit fullscreen mode

VS Code provides acquireVsCodeApi() inside the webview, but TypeScript does not know about it by default. This file adds the type information needed by webview-ui/src/api/vscode-api.ts.

The wrapper checks typeof acquireVsCodeApi === "function" because the function only exists inside VS Code, not when previewing the Vue app in a normal browser.

Create webview-ui/src/components/Webview.vue

Create webview-ui/src/components/Webview.vue:

<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from "vue";
import VSCodeButton from "./VSCodeButton.vue";
import VSCodeCard from "./VSCodeCard.vue";
import VSCodeTextArea from "./VSCodeTextArea.vue";
import VSCodeTextField from "./VSCodeTextField.vue";
import { getVsCodeState, postMessage, setVsCodeState } from "../api/vscode-api";

type AppState = {
  projectName: string;
  format: string;
  notes: string;
};

type ExtensionMessage =
  | {
      type: "settingsSaved";
      payload: {
        savedAt: string;
      };
    }
  | {
      type: "fromExtension";
      payload: {
        message: string;
      };
    };

const defaultState: AppState = {
  projectName: "Jupytext Pair Helper",
  format: "ipynb,py:percent",
  notes: "",
};

const savedState = getVsCodeState<AppState>();

const projectName = ref(savedState?.projectName ?? defaultState.projectName);
const format = ref(savedState?.format ?? defaultState.format);
const notes = ref(savedState?.notes ?? defaultState.notes);
const status = ref("Ready");

watch(
  [projectName, format, notes],
  () => {
    setVsCodeState({
      projectName: projectName.value,
      format: format.value,
      notes: notes.value,
    });
  },
  { immediate: true }
);

function saveSettings() {
  postMessage({
    type: "saveSettings",
    payload: {
      projectName: projectName.value,
      format: format.value,
      notes: notes.value,
    },
  });
}

function showInfoMessage() {
  postMessage({
    type: "showInfo",
    payload: {
      message: `Current project: ${projectName.value}`,
    },
  });
}

function resetSettings() {
  projectName.value = defaultState.projectName;
  format.value = defaultState.format;
  notes.value = defaultState.notes;
  status.value = "Reset locally. Click Save settings to send the update to VS Code.";
}

function handleMessage(event: MessageEvent<ExtensionMessage>) {
  const message = event.data;

  switch (message.type) {
    case "settingsSaved":
      status.value = `Settings saved at ${message.payload.savedAt}`;
      break;
    case "fromExtension":
      status.value = message.payload.message;
      break;
  }
}

onMounted(() => {
  window.addEventListener("message", handleMessage);
});

onUnmounted(() => {
  window.removeEventListener("message", handleMessage);
});
</script>

<template>
  <main class="min-h-screen p-5">
    <section class="mx-auto grid max-w-4xl gap-5">
      <VSCodeCard>
        <p class="text-xs uppercase tracking-wide text-[var(--vscode-descriptionForeground)]">
          VS Code Webview Tutorial
        </p>
        <h1 class="mt-2 text-2xl font-semibold text-[var(--vscode-foreground)]">
          Vue + Vite + Tailwind + VS Code Message Passing
        </h1>
        <p class="mt-2 max-w-2xl text-sm leading-6 text-[var(--vscode-descriptionForeground)]">
          This webview uses Vue for state, Tailwind for layout, and VS Code message
          passing to communicate with the extension.
        </p>
      </VSCodeCard>

      <div class="grid gap-5 md:grid-cols-2">
        <VSCodeCard>
          <h2 class="text-lg font-medium">Settings</h2>

          <div class="mt-4 grid gap-4">
            <label class="grid gap-1 text-sm">
              <span class="text-[var(--vscode-descriptionForeground)]">Project name</span>
              <VSCodeTextField v-model="projectName" />
            </label>

            <label class="grid gap-1 text-sm">
              <span class="text-[var(--vscode-descriptionForeground)]">Pairing format</span>
              <select
                v-model="format"
                class="w-full rounded border border-[var(--vscode-dropdown-border)] bg-[var(--vscode-dropdown-background)] px-3 py-2 text-sm text-[var(--vscode-dropdown-foreground)] focus:outline focus:outline-1 focus:outline-[var(--vscode-focusBorder)]"
              >
                <option value="ipynb,py:percent">Python percent pair</option>
                <option value="ipynb,md:myst">Markdown MyST pair</option>
                <option value="ipynb,md,pct.py:percent">
                  Notebook + Markdown + percent script
                </option>
              </select>
            </label>

            <label class="grid gap-1 text-sm">
              <span class="text-[var(--vscode-descriptionForeground)]">Notes</span>
              <VSCodeTextArea
                v-model="notes"
                placeholder="Notes about the selected pairing format..."
              />
            </label>

            <div class="flex flex-wrap gap-2">
              <VSCodeButton @click="saveSettings">Save settings</VSCodeButton>
              <VSCodeButton @click="showInfoMessage">Show VS Code message</VSCodeButton>
              <VSCodeButton variant="secondary" @click="resetSettings">Reset</VSCodeButton>
            </div>
          </div>
        </VSCodeCard>

        <VSCodeCard>
          <h2 class="text-lg font-medium">Preview</h2>

          <div class="mt-4 grid gap-4">
            <div class="rounded-lg border border-[var(--vscode-panel-border)] p-4">
              <p class="text-sm text-[var(--vscode-descriptionForeground)]">Project</p>
              <h3 class="mt-1 text-lg font-medium">{{ projectName }}</h3>
            </div>

            <div class="rounded-lg border border-[var(--vscode-panel-border)] p-4">
              <p class="text-sm text-[var(--vscode-descriptionForeground)]">
                Selected Jupytext format
              </p>
              <code class="mt-2 block rounded bg-[var(--vscode-textCodeBlock-background)] p-3">
                {{ format }}
              </code>
            </div>

            <div class="rounded-lg border border-[var(--vscode-panel-border)] p-4">
              <p class="text-sm text-[var(--vscode-descriptionForeground)]">Status</p>
              <p class="mt-1 text-sm">{{ status }}</p>
            </div>
          </div>
        </VSCodeCard>
      </div>
    </section>
  </main>
</template>

Enter fullscreen mode Exit fullscreen mode

How the interaction works

The Vue webview sends this message when the user clicks Save settings:

postMessage({
  type: "saveSettings",
  payload: {
    projectName: projectName.value,
    format: format.value,
    notes: notes.value,
  },
});

Enter fullscreen mode Exit fullscreen mode

The extension receives that message, runs extension-side logic, and sends a response back to Vue.

The Vue webview receives extension messages here:

window.addEventListener("message", handleMessage);

Enter fullscreen mode Exit fullscreen mode


8. Make App.vue Render Webview.vue

Keep App.vue small. Its job is to load the main webview component.

Create/Edit webview-ui/src/App.vue:

<script setup lang="ts">
import Webview from "./components/Webview.vue";
</script>

<template>
  <Webview />
</template>

Enter fullscreen mode Exit fullscreen mode

This keeps the main webview logic inside components/Webview.vue, while App.vue stays as the Vue app entry component.


9. Build the Vue App

From inside webview-ui, run:

npm run build

Enter fullscreen mode Exit fullscreen mode

Vite creates:

webview-ui/dist/
  index.html
  assets/
    index.js
    index.css

Enter fullscreen mode Exit fullscreen mode

The extension will load these built files into the webview.


10. Add the Webview Command to package.json

Open the extension root package.json.

Update contributes.commands:

{
  "contributes": {
    "commands": [
      {
        "command": "vue-webview-vite.openPanel",
        "title": "Open Vue Webview"
      }
    ]
  }
}

Enter fullscreen mode Exit fullscreen mode

Make sure the command name matches the command registered in extension.ts.


11. Create the Webview Panel in extension.ts

Replace src/extension.ts with:

import * as vscode from "vscode";

type WebviewMessage =
  | {
      type: "saveSettings";
      payload: {
        projectName: string;
        format: string;
        notes: string;
      };
    }
  | {
      type: "showInfo";
      payload: {
        message: string;
      };
    };

export function activate(context: vscode.ExtensionContext) {
  const disposable = vscode.commands.registerCommand(
    "vue-webview-vite.openPanel",
    () => {
      const panel = vscode.window.createWebviewPanel(
        "vueWebviewVite",
        "Vue Webview",
        vscode.ViewColumn.One,
        {
          enableScripts: true,
          localResourceRoots: [
            vscode.Uri.joinPath(context.extensionUri, "webview-ui", "dist"),
          ],
        }
      );

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

      panel.webview.onDidReceiveMessage(
        async (message: WebviewMessage) => {
          switch (message.type) {
            case "saveSettings": {
              await context.globalState.update(
                "vueWebviewVite.settings",
                message.payload
              );

              vscode.window.showInformationMessage(
                `Saved settings for ${message.payload.projectName}`
              );

              panel.webview.postMessage({
                type: "settingsSaved",
                payload: {
                  savedAt: new Date().toLocaleTimeString(),
                },
              });

              break;
            }

            case "showInfo": {
              vscode.window.showInformationMessage(message.payload.message);

              panel.webview.postMessage({
                type: "fromExtension",
                payload: {
                  message: "VS Code received the message and showed a notification.",
                },
              });

              break;
            }
          }
        },
        undefined,
        context.subscriptions
      );
    }
  );

  context.subscriptions.push(disposable);
}

function getWebviewHtml(webview: vscode.Webview, extensionUri: vscode.Uri) {
  const scriptUri = webview.asWebviewUri(
    vscode.Uri.joinPath(extensionUri, "webview-ui", "dist", "assets", "index.js")
  );

  const styleUri = webview.asWebviewUri(
    vscode.Uri.joinPath(extensionUri, "webview-ui", "dist", "assets", "index.css")
  );

  const nonce = getNonce();

  return /* html */ `
    <!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'; img-src ${webview.cspSource} https:; style-src ${webview.cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}';"
        />

        <link rel="stylesheet" href="${styleUri}" />
        <title>Vue Webview</title>
      </head>
      <body>
        <div id="app"></div>
        <script type="module" nonce="${nonce}" src="${scriptUri}"></script>
      </body>
    </html>
  `;
}

function getNonce() {
  let text = "";
  const possible =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

  for (let i = 0; i < 32; i++) {
    text += possible.charAt(Math.floor(Math.random() * possible.length));
  }

  return text;
}

export function deactivate() {}

Enter fullscreen mode Exit fullscreen mode

Important details:

  • enableScripts: true allows the Vue bundle to run.
  • localResourceRoots allows the webview to read files from webview-ui/dist.
  • webview.asWebviewUri(...) converts extension file paths into safe webview URLs.
  • context.extensionUri points to the extension root folder, not the out/ folder.
  • The compiled extension runs from out/extension.js, but it can still reference webview-ui/dist/assets/index.js relative to the extension root.
  • The CSP uses a nonce so only the intended script can run.

12. Run Both Development Watchers

From your terminal, make sure you are in the extension root folder:

cd vue-webview-vite

Enter fullscreen mode Exit fullscreen mode

For development, run the extension's TypeScript watcher and the Vite webview watcher at the same time.

You need both because they build different parts of the project:

Extension watcher → compiles src/extension.ts into out/extension.js
Webview watcher   → builds Vue/Tailwind files into webview-ui/dist/

Enter fullscreen mode Exit fullscreen mode

Use concurrently because both watcher commands are long-running processes.

From the extension root, install it:

npm install -D concurrently

Enter fullscreen mode Exit fullscreen mode

Add these scripts to the extension root package.json:

{
  "scripts": {
    "watch": "tsc -watch -p ./",
    "watch:webview": "npm --prefix webview-ui run build -- --watch",
    "dev": "concurrently \"npm run watch\" \"npm run watch:webview\""
  }
}

Enter fullscreen mode Exit fullscreen mode

Now start both watchers from the extension root:

npm run dev

Enter fullscreen mode Exit fullscreen mode

This keeps both outputs updated while you work:

out/extension.js
webview-ui/dist/assets/index.js
webview-ui/dist/assets/index.css

Enter fullscreen mode Exit fullscreen mode


13. Start and Reload the Extension

To start the extension:

  1. Open the extension project in VS Code.
  2. Press F5.
  3. A new Extension Development Host window opens.
  4. Open the Command Palette.
  5. Run:
Open Vue Webview

Enter fullscreen mode Exit fullscreen mode

The Vue webview should appear.

Reloading after changes

There are two common reload flows.

If you changed Vue code

  1. Keep npm run dev running from the extension root.
  2. Close the webview panel.
  3. Run Open Vue Webview again.

Usually you do not need to restart the full Extension Development Host for Vue-only changes.

If you changed extension code

If you changed src/extension.ts or package.json:

  1. Go to the Extension Development Host window.
  2. Press:
Ctrl + R

Enter fullscreen mode Exit fullscreen mode

On macOS, use:

Cmd + R

Enter fullscreen mode Exit fullscreen mode

This reloads the Extension Development Host.

Then run:

Open Vue Webview

Enter fullscreen mode Exit fullscreen mode

again from the Command Palette.


14. Production Build

Before packaging or publishing the extension, build both parts:

npm run compile
npm --prefix webview-ui run build

Enter fullscreen mode Exit fullscreen mode

You should commit or package the built webview assets depending on your publishing workflow.


15. Quick Checks

Before running the extension, confirm these files exist:

out/extension.js
webview-ui/dist/assets/index.js
webview-ui/dist/assets/index.css

Enter fullscreen mode Exit fullscreen mode

Also confirm the command ID matches in both places:

package.json → contributes.commands[].command
src/extension.ts → vscode.commands.registerCommand(...)

Enter fullscreen mode Exit fullscreen mode

If the webview is blank, open VS Code Developer Tools with:

Developer: Toggle Developer Tools

Enter fullscreen mode Exit fullscreen mode


16. Final Folder Structure

vue-webview-vite/
  package.json
  tsconfig.json
  src/
    extension.ts
  out/
    extension.js
    extension.js.map
  webview-ui/
    package.json
    vite.config.ts
    index.html
    src/
      App.vue
      main.ts
      style.css
      vscode.d.ts
      api/
        vscode-api.ts
      components/
        Webview.vue
        VSCodeButton.vue
        VSCodeCard.vue
        VSCodeTextArea.vue
        VSCodeTextField.vue
    dist/
      assets/
        index.js
        index.css

Enter fullscreen mode Exit fullscreen mode


Summary

You now have a VS Code extension that:

  • Registers a command in the Command Palette
  • Opens a custom webview panel
  • Loads a Vue app built by Vite
  • Uses Tailwind CSS for layout and styling
  • Uses small reusable Vue components styled with VS Code theme variables
  • Sends messages from the Vue webview to the VS Code extension
  • Sends messages from the extension back to the Vue webview
  • Supports a practical reload workflow during development

The key development loop is:

npm run dev

Enter fullscreen mode Exit fullscreen mode

Then:

F5 → Open Vue Webview → edit Vue → close/reopen panel → edit extension → Ctrl/Cmd + R

Enter fullscreen mode Exit fullscreen mode

Source Code

https://github.com/moyarich/vue-webview-vite