Chrome Extension Content Script Vue — Best Practices
3 min readContent Script Vue Pattern
Guide for integrating Vue.js into Chrome extension content scripts with proper isolation.
Shadow DOM Mounting
Content scripts run in the context of the host page, so style isolation is critical. Vue apps must mount inside a shadow DOM to prevent styles from bleeding into or out of your extension UI.
import { createApp, defineComponent } from 'vue';
function createShadowHost(): HTMLElement {
const host = document.createElement('div');
host.id = 'my-extension-root';
document.body.appendChild(host);
const shadow = host.attachShadow({ mode: 'open' });
return shadow as unknown as HTMLElement;
}
const App = defineComponent({
template: `<div class="widget">Hello Vue!</div>`,
styles: [`.widget { background: white; padding: 16px; }`]
});
const shadowHost = createShadowHost();
const app = createApp(App);
app.mount(shadowHost);
Vue 3 createApp in Content Scripts
Use defineComponent for type-safe components. Mount directly to the shadow root element, not the host. The runtime-only build is required since there’s no compile step in content scripts.
Scoped Styles
Shadow DOM provides automatic style isolation—extension styles won’t affect the page, and page styles won’t affect your app. Combine with CSS modules for component-level scoping:
import styles from './Widget.module.css';
const Widget = defineComponent({
module: { styles },
template: `<div :class="$style.widget">Content</div>`
});
Reactivity in Content Scripts
Vue’s reactivity system works normally inside shadow DOM. All features—composables, reactivity refs, computed properties—function as expected.
Communication with Background
Create a composable for messaging:
import { ref } from 'vue';
export function useExtensionMessaging() {
const response = ref(null);
function sendMessage(message: object) {
chrome.runtime.sendMessage(message, (res) => {
response.value = res;
});
}
return { sendMessage, response };
}
Pinia Store in Content Scripts
Pinia works in content scripts for shared state management. Keep stores lightweight since they’re loaded per page:
import { defineStore } from 'pinia';
export const useWidgetStore = defineStore('widget', {
state: () => ({ count: 0 }),
actions: {
increment() { this.count++; }
}
});
Bundle Considerations
Vue 3 runtime is ~30KB gzipped. Use runtime-only builds—no template compiler in content scripts. Configure Vite to exclude the compiler:
export default {
build: {
rollupOptions: {
external: ['vue']
}
}
}
Teleport Limitations
Vue teleport targets can’t escape shadow DOM boundaries. Keep all teleported content within the shadow root, or use fixed-position portals inside the shadow tree.
Building with Vite
Use vite-plugin-web-extension for building Vue content scripts. Configure it to output ESM and handle the Vue runtime:
import webExtension from 'vite-plugin-web-extension';
import vue from '@vitejs/plugin-vue';
export default {
plugins: [webExtension(), vue()]
}
Hot Reload Limitations
Content script hot reload is limited—changes may require page refresh. Use chrome.runtime.reload() carefully during development.
Alternative: Petite Vue
For simpler injections, consider Petite Vue (~6KB). It lacks full Vue features but works well for lightweight widgets.
Related Patterns
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.