Chrome Extension Content Script Frameworks — Developer Guide

5 min read

Using Frameworks in Content Scripts

Overview

Content scripts can inject full UI frameworks like React, Vue, Svelte, or Preact into web pages. This enables building complex floating panels, overlays, toolbars, and interactive widgets.

Why Use a Framework

Shadow DOM Foundation

Shadow DOM provides essential isolation for injected UI:

const host = document.createElement('div');
host.id = 'my-extension-root';
document.body.appendChild(host);
const shadow = host.attachShadow({ mode: 'closed' });
// Framework mounts inside shadow root

Key benefits: CSS isolation, correct event bubbling, and no style leakage.

React in Content Scripts

import { createRoot } from 'react-dom/client';
import { App } from './App';

const host = document.createElement('div');
host.id = 'react-extension-root';
document.body.appendChild(host);
const shadow = host.attachShadow({ mode: 'closed' });

const style = document.createElement('style');
style.textContent = '.button { background: #2563eb; color: white; padding: 8px 16px; }';
shadow.appendChild(style);

const root = createRoot(shadow);
root.render(<App />);

Preact alternative: Use Preact for smaller bundle (~3KB vs ~40KB):

import { h, render } from 'preact';
import { useState } from 'preact/hooks';

render(<App />, shadow);

Vue in Content Scripts

import { createApp } from 'vue';
import App from './App.vue';

const host = document.createElement('div');
host.id = 'vue-extension-root';
document.body.appendChild(host);
const shadow = host.attachShadow({ mode: 'closed' });

const mountPoint = document.createElement('div');
mountPoint.id = 'app';
shadow.appendChild(mountPoint);

createApp(App).mount(mountPoint);

Vue’s scoped styles work well with shadow DOM.

Svelte in Content Scripts

Svelte offers the smallest bundle size:

import Component from './Widget.svelte';

const host = document.createElement('div');
host.id = 'svelte-extension-root';
document.body.appendChild(host);
const shadow = host.attachShadow({ mode: 'closed' });

new Component({ target: shadow, props: { message: 'Hello!' } });

Svelte’s compiled styles are scoped by default—ideal for isolation.

Build Configuration

// esbuild.config.js
esbuild.build({
  entryPoints: ['src/content-ui/index.tsx'],
  bundle: true,
  outfile: 'dist/content-ui.js',
  external: [],  // Bundle all dependencies
  minify: true,
  treeShaking: true,
  target: ['chrome100'],
  loader: { '.tsx': 'tsx', '.ts': 'ts' },
  jsxFactory: 'h',
  jsxFragment: 'Fragment',
});

manifest.json:

{
  "content_scripts": [{
    "matches": ["<all_urls>"],
    "js": ["content-ui.js"],
    "run_at": "document_idle"
  }]
}

CSS Strategies

CSS-in-JS for Dynamic Injection

const styles = `.panel { position: fixed; top: 20px; right: 20px; background: white; }`;
const styleSheet = new CSSStyleSheet();
styleSheet.replaceSync(styles);
shadow.adoptedStyleSheets = [styleSheet];

Inline Styles

const style = document.createElement('style');
style.textContent = `...css content...`;
shadow.appendChild(style);

Communication with Service Worker

import { createMessenger } from "@theluckystrike/webext-messaging";

type Messages = {
  getPageData: { request: void; response: { title: string; url: string } };
};

const msg = createMessenger<Messages>();
const data = await msg.send("getPageData");

Cross-references

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