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

> Learn how CRXJS simplifies building Chrome and Firefox extensions. Discover its multi-browser support features for manifest background scripts and more.

- Repository: [crxjs/chrome-extension-tools](https://github.com/crxjs/chrome-extension-tools)
- Tags: deep-dive
- Published: 2026-02-28

---

**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`](https://github.com/crxjs/chrome-extension-tools/blob/main/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.

```typescript
// 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`](https://github.com/crxjs/chrome-extension-tools/blob/main/vite.config.ts), this value propagates through the entire build pipeline:

```typescript
// 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`](https://github.com/crxjs/chrome-extension-tools/blob/main/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`](https://github.com/crxjs/chrome-extension-tools/blob/main/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`:

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

```

**Firefox** utilizes a **background page** with a `scripts` array instead:

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

```

The plugin automatically generates the correct entry in [`manifest.json`](https://github.com/crxjs/chrome-extension-tools/blob/main/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`](https://github.com/crxjs/chrome-extension-tools/blob/main/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`](https://github.com/crxjs/chrome-extension-tools/blob/main/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)

```typescript
// 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

```typescript
// 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`](https://github.com/crxjs/chrome-extension-tools/blob/main/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`](https://github.com/crxjs/chrome-extension-tools/blob/main/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`](https://github.com/crxjs/chrome-extension-tools/blob/main/vite.chrome.config.ts) and [`vite.firefox.config.ts`](https://github.com/crxjs/chrome-extension-tools/blob/main/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.