Building a Chrome MV3 Extension with Vue 3 and Element Plus — with Live Reload

This guide walks through creating a Manifest V3 Chrome extension using Vite + Vue 3 + Element Plus,
and setting up an environment that supports instant rebuilds and automatic reload during development.


1. Project Structure

Create the following folders and files:

chrome-ext-vue-elementplus/
├─ public/
│   ├─ icons/               # extension icons
│   └─ manifest.json        # Chrome extension manifest
├─ src/
│   ├─ popup.html           # Popup entry
│   ├─ options.html         # Options entry
│   ├─ background.js        # Service Worker
│   ├─ content-script.js    # Content script
│   ├─ popup/
│   │   ├─ App.vue
│   │   └─ main.js
│   └─ options/
│       ├─ App.vue
│       └─ main.js
├─ package.json
└─ vite.config.js

Manifest

public/manifest.json

{
  "manifest_version": 3,
  "name": "MV3 + Vue + Element Plus Starter",
  "version": "0.1.0",
  "description": "A Chrome extension example built with Vite, Vue 3 and Element Plus.",
  "action": { "default_title": "Starter", "default_popup": "src/popup.html" },
  "background": { "service_worker": "assets/background.js", "type": "module" },
  "options_page": "src/options.html",
  "permissions": ["storage","activeTab","scripting","contextMenus"],
  "host_permissions": ["<all_urls>"],
  "content_scripts": [
    { "matches": ["<all_urls>"], "js": ["assets/content-script.js"], "run_at": "document_end" }
  ]
}

We place HTML files in src/, so manifest paths must begin with src/….
After building, Vite will output them to dist/src/.


2. Install Dependencies

npm init -y
npm install vue@3 element-plus
npm install -D vite @vitejs/plugin-vue web-ext npm-run-all wait-on
  • vite – build and watch
  • @vitejs/plugin-vue – Vue single-file component support
  • web-ext – runs Chrome with the extension and reloads on changes
  • npm-run-all & wait-on – start tasks in sequence to avoid race conditions

3. Vite Configuration

vite.config.js

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'node:path'

export default defineConfig({
  base: './',   // relative paths required inside Chrome extension
  plugins: [vue()],
  build: {
    outDir: 'dist',
    emptyOutDir: true,
    rollupOptions: {
      input: {
        popup: resolve(__dirname, 'src/popup.html'),
        options: resolve(__dirname, 'src/options.html'),
        background: resolve(__dirname, 'src/background.js'),
        'content-script': resolve(__dirname, 'src/content-script.js')
      },
      output: {
        entryFileNames: 'assets/[name].js',
        chunkFileNames: 'assets/[name].js',
        assetFileNames: 'assets/[name][extname]'
      }
    }
  }
})

4. Vue + Element Plus Pages

Popup Entry

src/popup.html

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Popup</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="./popup/main.js"></script>
  </body>
</html>

Options Entry

src/options.html

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Options</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="./options/main.js"></script>
  </body>
</html>

Popup Component

src/popup/App.vue

<template>
  <div style="width:320px;max-width:320px;padding:16px">
    <el-form label-position="top">
      <el-form-item label="Keyword">
        <el-input v-model="keyword" placeholder="Enter a keyword to highlight" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="apply">Highlight</el-button>
        <el-button @click="toggle">Toggle Highlight</el-button>
      </el-form-item>
      <el-form-item>
        <el-button @click="getInfo">Page Info</el-button>
        <el-button @click="openOptions">Open Settings</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { ElMessageBox, ElNotification } from 'element-plus'

const keyword = ref('')

async function apply() {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
  await chrome.storage.local.set({ keyword: keyword.value })
  if (tab?.id) {
    chrome.tabs.sendMessage(tab.id, { type: 'HIGHLIGHT_SET', keyword: keyword.value })
  }
  ElNotification({ message: 'Highlight applied', type: 'success' })
}

async function toggle() {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
  if (tab?.id) chrome.tabs.sendMessage(tab.id, { type: 'HIGHLIGHT_TOGGLE' })
}

async function getInfo() {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
  if (!tab?.id) return
  try {
    const info = await chrome.tabs.sendMessage(tab.id, { type: 'PAGE_INFO' })
    ElMessageBox.alert(JSON.stringify(info, null, 2), 'Page Information')
  } catch {
    ElNotification({ message: 'Failed to retrieve page information', type: 'error' })
  }
}

function openOptions() {
  chrome.runtime.openOptionsPage()
}
</script>

src/popup/main.js

import 'element-plus/dist/index.css'
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import App from './App.vue'

console.log('[popup] main.js loaded')

const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')

Options Component

src/options/App.vue

<template>
  <div style="padding:16px">
    <h2>Extension Settings</h2>
    <el-form label-position="top">
      <el-form-item label="Default Keyword">
        <el-input v-model="kw" placeholder="Enter default keyword" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="save">Save</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { ElNotification } from 'element-plus'

const kw = ref('')

onMounted(async () => {
  const { keyword } = await chrome.storage.local.get(['keyword'])
  kw.value = keyword || ''
})

async function save() {
  await chrome.storage.local.set({ keyword: kw.value.trim() })
  ElNotification({ message: 'Saved successfully', type: 'success' })
}
</script>

src/options/main.js

import 'element-plus/dist/index.css'
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import App from './App.vue'

console.log('[options] main.js loaded')

const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')

5. Building and Manual Load

For the first run, build once:

npm run build

Open chrome://extensions, enable Developer Mode, click Load unpacked,
and select the dist/ folder.

The extension is now usable, with Vue + Element Plus UI for both Popup and Options pages.


6. Live-Reload Development

To avoid rebuilding and reloading manually on every change:

  1. package.json scripts
{
  "name": "chrome-ext-mv3-vue-elementplus",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "vite build",
    "watch": "vite build --watch",
    "serve": "wait-on dist/assets/content-script.js && web-ext run --target=chromium --source-dir=dist",
    "dev": "npm-run-all --parallel watch serve"
  },
  "dependencies": {
    "element-plus": "^2.8.6",
    "vue": "^3.4.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.0",
    "concurrently": "^9.2.1",
    "npm-run-all": "^4.1.5",
    "vite": "^5.4.0",
    "wait-on": "^8.0.4",
    "web-ext": "^8.9.0"
  }
}
  1. Start development mode:
npm run dev
  • vite build --watch continuously rebuilds files into dist/
  • wait-on ensures the first build finishes before launching Chrome
  • web-ext run loads the extension and automatically reloads it whenever the dist/ folder changes

Now you can simply save a file and see Chrome refresh the extension instantly.


7. Debugging Tips

  • Popup / Options – Right-click → Inspect
  • Service Workerchrome://extensions → Details → Service WorkerInspect
  • Content script – Use the DevTools console on any matched web page.

8. Summary

  • Vite + Vue + Element Plus provides a modern, component-based UI for Chrome extensions.
  • Use base: './' in Vite to generate relative asset paths that work inside MV3.
  • Combine vite build --watch with web-ext for save → build → auto-reload development.
  • Vue SFC files keep templates, logic and style organized for future maintenance.

With this setup you can focus on building features while enjoying instant feedback and a clean Element Plus interface inside your Chrome extension.

Leave a Reply

Your email address will not be published. Required fields are marked *