Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/crxjs/chrome-extension-tools/llms.txt

Use this file to discover all available pages before exploring further.

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:
vite.config.ts
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:
svelte.config.js
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'

export default {
  preprocess: vitePreprocess(),
}

TypeScript Configuration

Set up TypeScript for Svelte:
tsconfig.json
{
  "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:
src/popup/main.ts
import { mount } from 'svelte'
import App from './App.svelte'
import './style.css'

const app = mount(App, {
  target: document.getElementById('app'),
})

export default app
src/popup/App.svelte
<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:
src/content/main.ts
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:
manifest.config.ts
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:
stores/storage.ts
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

package.json
{
  "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

Build docs developers (and LLMs) love