CRXJS supports building extensions for both Chrome and Firefox. This guide covers browser-specific differences and how CRXJS handles them.
Configuration
Set the target browser in your CRXJS configuration:
import { crx } from '@crxjs/vite-plugin'
import manifest from './manifest.json'
crx({
manifest,
browser: 'chrome' // or 'firefox'
})
Target browser for the extension build.Default: 'chrome'
Browser Differences
CRXJS automatically handles browser-specific manifest and implementation differences:
Background Scripts
Chrome
// manifest.json (Chrome)
{
"background": {
"service_worker": "background.js",
"type": "module"
}
}
- Uses Service Worker API
- No persistent background page
- Must handle lifecycle events
- Module imports supported
Firefox
// manifest.json (Firefox)
{
"background": {
"scripts": ["background.js"],
"type": "module"
}
}
- Uses background scripts array
- Supports
persistent: false for event pages
- Different module loading syntax during development
CRXJS automatically transforms your manifest’s background configuration based on the target browser.
Development differences:
// Chrome (development)
import 'http://localhost:5173/@vite/env'
import 'http://localhost:5173/worker-client.js'
import 'http://localhost:5173/background.ts'
// Firefox (development) - uses dynamic imports
import('http://localhost:5173/@vite/env')
import('http://localhost:5173/worker-client.js')
import('http://localhost:5173/background.ts')
Web Accessible Resources
Chrome
{
"web_accessible_resources": [
{
"matches": ["<all_urls>"],
"resources": ["assets/*"],
"use_dynamic_url": true
}
]
}
- Supports
use_dynamic_url for security
- Extension origin changes on each reload when enabled
Firefox
{
"web_accessible_resources": [
{
"matches": ["<all_urls>"],
"resources": ["assets/*"]
}
]
}
- Does not support
use_dynamic_url
- CRXJS automatically removes this field for Firefox
- Firefox handles dynamic URLs by default
CRXJS automatically strips use_dynamic_url from the manifest when building for Firefox, as Firefox doesn’t support this field.
Browser-Specific Settings
For Firefox, you may need additional manifest fields:
{
"browser_specific_settings": {
"gecko": {
"id": "[email protected]",
"strict_min_version": "109.0"
}
}
}
Firefox requires a unique extension ID in browser_specific_settings.gecko.id. This is mandatory for Firefox Add-ons (AMO) submissions.
Multi-Browser Build
Build for both browsers using Vite modes:
Configuration
// vite.config.ts
import { defineConfig } from 'vite'
import { crx } from '@crxjs/vite-plugin'
import manifest from './manifest.json'
export default defineConfig(({ mode }) => {
const browser = mode === 'firefox' ? 'firefox' : 'chrome'
return {
plugins: [
crx({
manifest,
browser
})
],
build: {
outDir: `dist/${browser}`
}
}
})
Build Commands
// package.json
{
"scripts": {
"dev": "vite",
"dev:firefox": "vite --mode firefox",
"build": "vite build",
"build:firefox": "vite build --mode firefox",
"build:all": "npm run build && npm run build:firefox"
}
}
Run builds:
# Chrome
npm run build
# Firefox
npm run build:firefox
# Both
npm run build:all
Dynamic Manifest
Use a function to customize the manifest per browser:
// vite.config.ts
import { defineConfig } from 'vite'
import { crx } from '@crxjs/vite-plugin'
import baseManifest from './manifest.json'
export default defineConfig(({ mode }) => {
const isFirefox = mode === 'firefox'
return {
plugins: [
crx({
browser: isFirefox ? 'firefox' : 'chrome',
manifest: (env) => {
const manifest = { ...baseManifest }
// Firefox-specific adjustments
if (isFirefox) {
manifest.browser_specific_settings = {
gecko: {
id: '[email protected]',
strict_min_version: '109.0'
}
}
// Firefox doesn't support some Chrome features
delete manifest.side_panel
}
return manifest
}
})
],
build: {
outDir: `dist/${isFirefox ? 'firefox' : 'chrome'}`
}
}
})
Runtime Browser Detection
Detect the browser in your extension code:
// utils/browser.ts
export const isFirefox = typeof browser !== 'undefined'
export const isChrome = typeof chrome !== 'undefined' && !isFirefox
// Or check user agent
export function getBrowser() {
const agent = navigator.userAgent.toLowerCase()
if (agent.includes('firefox')) return 'firefox'
if (agent.includes('chrome')) return 'chrome'
if (agent.includes('edg')) return 'edge'
return 'unknown'
}
Use browser-specific APIs:
import { isFirefox } from './utils/browser'
if (isFirefox) {
// Firefox-specific code
browser.storage.local.get('key')
} else {
// Chrome-specific code
chrome.storage.local.get('key')
}
API Compatibility
Chrome APIs vs WebExtension APIs
| Feature | Chrome | Firefox | CRXJS Handling |
|---|
| Background | Service Worker | Scripts | Auto-configured |
| Promises | Callback-based | Promise-based | No change needed |
| MV3 Support | Full | Partial | Use compatible features |
use_dynamic_url | Supported | Not supported | Auto-removed for Firefox |
side_panel | Supported | Not supported | Chrome-only |
Recommended: Use Promises
Modern Chrome supports promises. Use them for better compatibility:
// Good - Works in both
const data = await chrome.storage.local.get('key')
// Avoid - Callback style
chrome.storage.local.get('key', (result) => {
// ...
})
Testing Both Browsers
Chrome
- Build:
npm run build
- Open
chrome://extensions
- Enable “Developer mode”
- Click “Load unpacked”
- Select
dist/chrome
Firefox
- Build:
npm run build:firefox
- Open
about:debugging#/runtime/this-firefox
- Click “Load Temporary Add-on”
- Select
dist/firefox/manifest.json
Use web-ext CLI for Firefox development:npm install --save-dev web-ext
web-ext run --source-dir dist/firefox
Browser-Specific Features
Chrome-Only Features
// Only include these in Chrome builds
if (import.meta.env.MODE !== 'firefox') {
manifest.side_panel = {
default_path: 'sidepanel.html'
}
}
Firefox-Only Features
// Firefox-specific permissions
if (import.meta.env.MODE === 'firefox') {
manifest.permissions.push('tabs') // Firefox needs explicit tabs permission
}
Known Differences
Manifest V3 Support
- Chrome: Full MV3 support
- Firefox: MV3 support improving, some features still in development
- Some MV2 APIs still available in Firefox
Service Workers
- Chrome: Background service workers are required
- Firefox: Still uses background scripts, service worker support limited
Web Accessible Resources
- Chrome: Strict security with
use_dynamic_url
- Firefox: Simpler model without dynamic URLs
Best Practices
- Test both browsers regularly during development
- Use webextension-polyfill for API compatibility
- Avoid browser-specific features when possible
- Use feature detection instead of browser detection
- Separate browser-specific code into modules
// Good - Feature detection
if ('serviceWorker' in navigator) {
// Service worker code
}
// Avoid - Browser detection
if (isChrome) {
// Chrome-specific code
}
Environment Variables
Set browser-specific environment variables:
# .env.chrome
VITE_BROWSER=chrome
VITE_EXTENSION_ID=chrome-extension-id
# .env.firefox
VITE_BROWSER=firefox
VITE_EXTENSION_ID=firefox-extension-id
Access in code:
const browser = import.meta.env.VITE_BROWSER
const extensionId = import.meta.env.VITE_EXTENSION_ID
See Also