How CRXJS Handles Multi-Browser Support: Building Chrome and Firefox Extensions with One Config

The CRXJS Vite plugin accommodates multi-browser support through a configurable browser option that conditionally transforms manifest background scripts, web-accessible resources, and browser-specific settings to meet Chrome and Firefox requirements.

The crxjs/chrome-extension-tools repository provides a browser-agnostic architecture for developing browser extensions using Vite. By specifying a single configuration property, developers can target both Chromium-based browsers and Firefox without maintaining separate codebases or manifest files.

The Browser Configuration Option

At the heart of CRXJS multi-browser support lies the browser option defined in packages/vite-plugin/src/node/types.ts (lines 90-97). This option accepts a literal type of 'chrome' | 'firefox' and defaults to 'chrome' when unspecified.

// packages/vite-plugin/src/node/types.ts
export type Browser = 'chrome' | 'firefox'

export interface CrxOptions {
  // ... other options
  browser?: Browser // defaults to 'chrome'
}

When you initialize the plugin in your vite.config.ts, this value propagates through the entire build pipeline:

// vite.config.ts
import { defineConfig } from 'vite'
import { crx } from '@crxjs/vite-plugin'

export default defineConfig({
  plugins: [
    crx({
      manifest: './src/manifest.json',
      browser: 'firefox', // Target Gecko engine
    }),
  ],
})

Centralized Option Distribution

The pluginOptionsProvider (packages/vite-plugin/src/node/plugin-optionsProvider.ts, lines 33-48) acts as the distribution hub for the browser setting. During Vite's config hook, this provider injects the CrxOptions object into the plugin API, making opts.browser accessible to every internal sub-plugin.

This centralized approach ensures consistent behavior across the build process. Rather than each plugin parsing configuration independently, they retrieve the target browser from a single source of truth via the options provider.

Background Script Adaptation

One of the most significant differences between Chrome and Firefox lies in background script handling. The plugin-background.ts file (lines 58-71) contains conditional logic that detects the target browser and emits the appropriate manifest structure.

Chrome requires a service worker specified under manifest.background.service_worker:

{
  "background": {
    "service_worker": "loader.js",
    "type": "module"
  }
}

Firefox utilizes a background page with a scripts array instead:

{
  "background": {
    "scripts": ["loader.js"],
    "type": "module"
  }
}

The plugin automatically generates the correct entry in manifest.json based on the opts.browser value, ensuring compatibility with each browser's extension architecture.

Web-Accessible Resources and Feature Toggles

Chrome and Firefox handle web-accessible resources differently, particularly regarding the use_dynamic_url flag. Chrome requires this flag for runtime URL modifications, while Firefox automatically grants web-accessible URLs and rejects manifests containing Chrome-specific flags.

The plugin-webAccessibleResources.ts module (lines 63-71) manages these discrepancies:

  • Strips the use_dynamic_url flag when browser === 'firefox'
  • Merges duplicate resource entries to prevent conflicts
  • Removes the entire web_accessible_resources field for Firefox builds when appropriate

Additionally, the plugin generates default development entries with matches: ['<all_urls>'] to facilitate HMR (Hot Module Replacement) during development, adapting the output based on the target browser's permission model.

Manifest Schema Adaptations

The manifest.ts file (lines 82-88) defines the complete ManifestV3 type structure, including the optional browser_specific_settings.gecko block. When building for Firefox, CRXJS automatically includes this section containing Gecko-specific permissions, IDs, and update URLs.

For Chrome builds, the plugin omits the browser_specific_settings field entirely, producing a clean manifest that complies with the Chrome Web Store's strict schema validation. This conditional emission prevents validation errors that would otherwise cause extension submission rejections.

Practical Configuration Examples

Targeting Chrome (Default Behavior)

// vite.config.ts
import { defineConfig } from 'vite'
import { crx } from '@crxjs/vite-plugin'

export default defineConfig({
  plugins: [
    crx({
      manifest: './src/manifest.json',
      // browser defaults to 'chrome'
    }),
  ],
})

This configuration generates:

  • background.service_worker entries
  • use_dynamic_url flags in web-accessible resources
  • No browser_specific_settings block

Targeting Firefox Explicitly

// vite.config.ts
import { defineConfig } from 'vite'
import { crx } from '@crxjs/vite-plugin'

export default defineConfig({
  plugins: [
    crx({
      manifest: './src/manifest.json',
      browser: 'firefox',
    }),
  ],
})

This configuration generates:

  • background.scripts arrays instead of service workers
  • browser_specific_settings.gecko blocks
  • Stripped Chrome-specific flags from resources

Summary

  • Single Configuration Point: The browser option in CrxOptions serves as the single source of truth for target browser selection.
  • Automatic Manifest Transformation: CRXJS rewrites manifest.json background sections, resource flags, and browser-specific settings based on the target.
  • Background Architecture Handling: The plugin switches between Chrome's service workers and Firefox's background pages automatically.
  • Feature-Level Compatibility: Chrome-only flags like use_dynamic_url are removed for Firefox builds to prevent validation errors.
  • Type Safety: The Browser type restricts inputs to 'chrome' | 'firefox', preventing invalid target specifications.

Frequently Asked Questions

Does CRXJS support Safari or Edge extensions?

CRXJS explicitly supports Chrome and Firefox through the browser option. While Microsoft Edge utilizes Chromium and generally accepts Chrome-compatible manifests, Safari requires significant architectural differences and is not currently supported by the browser configuration type.

What happens if I omit the browser option in my Vite config?

If you omit the browser property, the plugin defaults to 'chrome' as defined in packages/vite-plugin/src/node/types.ts (line 38). All Firefox-specific transformations are bypassed, and the build generates a standard Chrome extension manifest with service workers and dynamic URL flags intact.

Can I build for both browsers simultaneously in one Vite command?

The current architecture requires separate build processes for each browser. You should create distinct Vite configuration files (e.g., vite.chrome.config.ts and vite.firefox.config.ts) with their respective browser values, then run builds sequentially or in parallel using separate CLI commands.

Why does Firefox use background scripts instead of service workers?

Firefox's implementation of Manifest V3 maintains support for background pages with persistent scripts rather than adopting Chrome's non-persistent service worker model. CRXJS detects the Firefox target and generates the scripts array format that Gecko requires, while Chrome receives the standard service_worker entry.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →