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 withsrc/…
.
After building, Vite will output them todist/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:
- 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" } }
- Start development mode:
npm run dev
vite build --watch
continuously rebuilds files intodist/
wait-on
ensures the first build finishes before launching Chromeweb-ext run
loads the extension and automatically reloads it whenever thedist/
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 Worker –
chrome://extensions
→ Details → Service Worker → Inspect - 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
withweb-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.
Recent Comments