Chrome Extension Content Script Vue — Best Practices

3 min read

Content 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.

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.