CRXJS works seamlessly with Svelte, providing hot module replacement (HMR) and all the modern development features you expect from Vite.
Installation
Install the required dependencies:
npm install svelte
npm install -D @sveltejs/vite-plugin-svelte
Vite Configuration
Configure Vite to use both the Svelte and CRXJS plugins:
import path from 'node:path'
import { crx } from '@crxjs/vite-plugin'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import { defineConfig } from 'vite'
import manifest from './manifest.config'
export default defineConfig({
resolve: {
alias: {
'@': `${path.resolve(__dirname, 'src')}`,
},
},
plugins: [
svelte({
compilerOptions: {
dev: true,
},
}),
crx({ manifest }),
],
server: {
cors: {
origin: [
/chrome-extension:\/\//,
],
},
},
})
The svelte() plugin must be placed before crx() in the plugins array.
Svelte Configuration
Create a svelte.config.js file:
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
export default {
preprocess: vitePreprocess(),
}
TypeScript Configuration
Set up TypeScript for Svelte:
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"types": ["vite/client", "chrome"],
"strict": true,
"skipLibCheck": true
},
"include": ["src"]
}
Create a Svelte popup component:
import { mount } from 'svelte'
import App from './App.svelte'
import './style.css'
const app = mount(App, {
target: document.getElementById('app'),
})
export default app
<script lang='ts'>
import CrxLogo from '@/assets/crx.svg'
import svelteLogo from '@/assets/svelte.svg'
import viteLogo from '@/assets/vite.svg'
import HelloWorld from '@/components/HelloWorld.svelte'
</script>
<div>
<a href='https://vite.dev' target='_blank'>
<img src={viteLogo} class='logo' alt='Vite logo'>
</a>
<a href='https://svelte.dev' target='_blank'>
<img src={svelteLogo} class='logo svelte' alt='Svelte logo'>
</a>
<a href='https://crxjs.dev/vite-plugin' target='_blank'>
<img src={CrxLogo} class='logo crx' alt='crx logo'>
</a>
</div>
<HelloWorld msg='Vite + Svelte + CRXJS' />
<style>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.svelte:hover {
filter: drop-shadow(0 0 2em #ff3e00aa);
}
.logo.crx:hover {
filter: drop-shadow(0 0 2em #f2bae4aa);
}
</style>
Content Script with Svelte
Inject Svelte into a webpage using a content script:
import { mount } from 'svelte'
import App from './views/App.svelte'
import './style.css'
console.log('[CRXJS] Hello world from content script!')
const container = document.createElement('div')
container.id = 'crxjs-app'
document.body.appendChild(container)
const app = mount(App, {
target: container,
})
export default app
Update your manifest to include the content script:
import { defineManifest } from '@crxjs/vite-plugin'
export default defineManifest({
manifest_version: 3,
name: 'My Svelte Extension',
version: '1.0.0',
action: {
default_popup: 'src/popup/index.html',
},
content_scripts: [{
js: ['src/content/main.ts'],
matches: ['https://*/*'],
}],
})
Hot Module Replacement
CRXJS provides full HMR support for Svelte:
- Component changes update instantly with state preservation
- Style changes apply immediately
- Manifest changes automatically reload the extension
Svelte HMR works out of the box with CRXJS. Make changes to your components and see them update in real-time without losing state.
Reactive Chrome APIs
Use Svelte’s reactivity with Chrome APIs:
<script lang="ts">
import { onMount } from 'svelte'
let tabs: chrome.tabs.Tab[] = []
onMount(async () => {
tabs = await chrome.tabs.query({ currentWindow: true })
})
async function closeTab(tabId: number) {
await chrome.tabs.remove(tabId)
tabs = tabs.filter(tab => tab.id !== tabId)
}
</script>
<div>
<h2>Open Tabs</h2>
<ul>
{#each tabs as tab (tab.id)}
<li>
{tab.title}
<button on:click={() => closeTab(tab.id!)}>Close</button>
</li>
{/each}
</ul>
</div>
Stores for Chrome Storage
Create Svelte stores that sync with Chrome storage:
import { writable } from 'svelte/store'
export function chromeStorage<T>(key: string, defaultValue: T) {
const { subscribe, set, update } = writable<T>(defaultValue)
// Load initial value
chrome.storage.sync.get([key], (result) => {
set(result[key] ?? defaultValue)
})
// Listen for changes from other contexts
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'sync' && changes[key]) {
set(changes[key].newValue)
}
})
return {
subscribe,
set: (value: T) => {
chrome.storage.sync.set({ [key]: value })
set(value)
},
update: (fn: (value: T) => T) => {
update((current) => {
const newValue = fn(current)
chrome.storage.sync.set({ [key]: newValue })
return newValue
})
},
}
}
Use it in your components:
<script lang="ts">
import { chromeStorage } from '@/stores/storage'
const theme = chromeStorage('theme', 'light')
function toggleTheme() {
$theme = $theme === 'light' ? 'dark' : 'light'
}
</script>
<button on:click={toggleTheme}>
Switch to {$theme === 'light' ? 'dark' : 'light'} mode
</button>
Svelte 5 Runes
If you’re using Svelte 5, you can leverage runes for even more powerful reactivity:
<script lang="ts">
let count = $state(0)
let doubled = $derived(count * 2)
async function saveCount() {
await chrome.storage.sync.set({ count })
}
$effect(() => {
chrome.storage.sync.get(['count'], (result) => {
count = result.count ?? 0
})
})
</script>
<div>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onclick={() => count++}>Increment</button>
<button onclick={saveCount}>Save</button>
</div>
Package.json Scripts
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"svelte": "^5.0.0"
},
"devDependencies": {
"@crxjs/vite-plugin": "latest",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tsconfig/svelte": "^5.0.0",
"@types/chrome": "^0.0.313",
"typescript": "~5.7.0",
"vite": "^6.0.0"
}
}
Next Steps