A modern Chrome extension starter template powered by Vite, Svelte 5, TypeScript, TailwindCSS, and DaisyUI. This template is designed for rapid development of MV3 (Manifest Version 3) extensions with a focus on performance, modularity, and modern developer tools.
Before you begin, ensure you have the following installed:
Clone the Repository:
git clone https://github.com/your-username/chrome-extension-starter.git
cd chrome-extension-starter
Install Dependencies:
Using npm:
npm install
Or using pnpm:
pnpm install
Run Development Server:
npm run dev
This will start the Vite dev server for live reloading and hot module replacement (HMR). The extension will be pre-rendered for Chrome MV3 development.
Build for Production:
npm run build
The production-ready extension will be output to the dist/ directory.
chrome://extensions in your browser.dist/ folder generated by the build process..
├── public/ # Static assets (manifest.json, icons)
├── src/
│ ├── background/ # Background scripts
│ ├── content/ # Content scripts for injecting into web pages
│ ├── popup/ # Popup UI components
│ ├── lib/ # Reusable components and utilities
│ ├── options/ # Options page components
│ ├── styles/ # TailwindCSS styles
│ ├── types/ # TypeScript declarations
│ └── main.ts # Entry point for the application
├── tailwind.config.js # TailwindCSS configuration
├── tsconfig.json # TypeScript configuration
├── vite.config.ts # Vite configuration
├── postcss.config.js # PostCSS plugins (for TailwindCSS)
└── package.json # Project dependencies and scripts
The manifest.json file is located in the public/ directory and defines the Chrome extension’s permissions and entry points.
Key Settings:
{
"manifest_version": 3,
"name": "Chrome Extension Starter",
"version": "0.0.1",
"description": "A modern Chrome extension template with Svelte, Vite, TypeScript, TailwindCSS, and DaisyUI.",
"action": {
"default_popup": "popup/index.html",
"default_icon": "icons/icon-128.png"
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content/index.js"]
}
],
"permissions": ["storage", "tabs"],
"icons": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
}
Customizing Tailwind:
Edit the tailwind.config.js file to add your themes, colors, or plugins.
module.exports = {
content: ["./src/**/*.{html,js,svelte,ts}"],
theme: {
extend: {},
},
plugins: [require("daisyui")],
};
DaisyUI Example:
<script>
let count = $state(0);
</script>
<div class="p-4 bg-base-200">
<button class="btn btn-primary" onclick={() => count++}>
Increment: {count}
</button>
</div>
npm run dev: Start the development server with HMR.npm run build: Build the extension for production.If you don't have just installed, you can use the shell scripts in the scripts/ directory:
# Install dependencies
./scripts/install.sh
# Start development server
./scripts/dev.sh
# Build for production
./scripts/build.sh
# Clean build artifacts
./scripts/clean.sh
# Show project status
./scripts/status.sh
# Prepare for Chrome Web Store
./scripts/prepare-publish.sh
# Quick test (rebuild + open Chrome)
./scripts/test.sh
# Show all available scripts
./scripts/help.sh
All scripts include:
Note: Scripts require chmod +x scripts/*.sh to be executable.
vite.config.ts)The project uses Vite for bundling. Customizations can be added in vite.config.ts.
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
export default defineConfig({
plugins: [svelte()],
build: {
rollupOptions: {
input: {
popup: "./src/popup/index.html",
background: "./src/background/index.ts",
content: "./src/content/index.ts",
},
},
},
});
This template includes a complete solution for CSS style isolation using Shadow DOM, addressing the challenges discussed in sveltejs/svelte#5869.
When building Chrome extensions that inject UI into web pages (content scripts), you need style isolation to prevent:
import { createApp } from './lib/utils/createApp';
import MyComponent from './lib/components/MyComponent.svelte';
// Mount with shadow DOM and automatic CSS injection
const app = createApp(MyComponent, {
target: '#app',
useShadowDOM: true,
injectStyles: true, // Auto-injects Tailwind, DaisyUI, and Lucide icons
});
// Cleanup when done
app.cleanup();
<link> tags for older browsers// content.ts - Inject your extension UI into any webpage
import { createApp } from '../lib/utils/createApp';
import Widget from '../lib/components/Widget.svelte';
// Create isolated container
const container = document.createElement('div');
container.id = 'my-extension-root';
container.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 999999;
`;
document.body.appendChild(container);
// Mount with complete CSS isolation
const app = createApp(Widget, {
target: container,
useShadowDOM: true,
injectStyles: true,
cssUrls: [
chrome.runtime.getURL('assets/main.css'), // Your bundled CSS
],
});
import { mountInShadow } from './lib/utils/shadowDOM';
import MyComponent from './lib/components/MyComponent.svelte';
const result = mountInShadow(MyComponent, {
target: document.getElementById('app'),
mode: 'open',
injectStyles: true,
});
// Access shadow root for advanced use cases
console.log('Shadow root:', result.shadowRoot);
result.cleanup();
import { injectCSS, getBundledCSSUrl } from './lib/utils/cssInjector';
const shadowRoot = container.attachShadow({ mode: 'open' });
// Inject CSS with Constructable Stylesheets (fastest)
await injectCSS({
shadowRoot,
cssUrls: [getBundledCSSUrl()],
adoptedStyleSheets: true,
});
import { createApps, cleanupApps } from './lib/utils/createApp';
const apps = createApps(Widget, [
{ target: '#widget1', useShadowDOM: true, injectStyles: true },
{ target: '#widget2', useShadowDOM: true, injectStyles: true },
{ target: '#widget3', useShadowDOM: true, injectStyles: true },
]);
// Cleanup all at once
cleanupApps(apps);
createApp(Component, options)Main utility for mounting Svelte components with optional shadow DOM.
Options:
target: HTMLElement or CSS selectoruseShadowDOM: Enable shadow DOM (default: false)shadowMode: 'open' or 'closed' (default: 'open')injectStyles: Auto-inject CSS (default: true)cssUrls: Additional CSS files to inject (default: [])props: Component props (default: {})Returns: { component, shadowRoot?, cleanup }
mountInShadow(Component, options)Lower-level API for manual shadow DOM control.
Options:
target: HTMLElementmode: 'open' or 'closed' (default: 'open')injectStyles: Auto-inject CSS (default: true)styleSheets: CSS URLs to inject (default: [])Returns: { shadowRoot, component, cleanup }
injectCSS(options)Inject CSS into a shadow root using Constructable Stylesheets or <link> tags.
Options:
shadowRoot: ShadowRoot targetcssUrls: Array of CSS file URLsinlineCss: Inline CSS stringadoptedStyleSheets: Use Constructable Stylesheets (default: true)Returns: Promise<void>
Check out ShadowDOMDemo.svelte for a complete working example that demonstrates:
Solution: Ensure injectStyles: true is enabled. The utility automatically injects all necessary CSS including Lucide icon definitions.
const app = createApp(Component, {
target: '#app',
useShadowDOM: true,
injectStyles: true, // ← This is critical
});
Problem: CSS not loading in shadow root.
Solutions:
getBundledCSSUrl()cloneDocumentStyles(shadowRoot) as a fallbackSolution: Wrap your component in a themed container:
<div data-theme="dracula">
<!-- Your component content -->
</div>
See shadowDOMExample.ts for 7 complete examples including:
If you encounter any issues or bugs, please file an issue in the GitHub repository.
If you find this project helpful, consider buying me a coffee!
This project is licensed under the MIT License.
Trent Brew