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 --watchcontinuously rebuilds files intodist/wait-onensures the first build finishes before launching Chromeweb-ext runloads 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 --watchwithweb-extfor 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