Skip to main content
Avoid maintaining separate manifest files for every browser. Extension.js lets you define browser-specific values inline with prefixes, then emits only the fields that match the active target at compile time.

Why this matters

Browsers still differ in key manifest areas, like background configuration and vendor metadata. Prefixed fields let you keep one source manifest.json while producing browser-correct output for Chromium-family and Firefox-family targets.

How it works

Extension.js scans manifest keys and resolves prefixed entries for the selected browser. A prefix names an engine family or one browser:
  • chromium: reaches every Chromium-family target (chromium, chrome, edge, chromium-based, the forks brave, opera, vivaldi, yandex, and Safari builds)
  • chrome: reaches only chrome, and edge: reaches only edge
  • firefox: and gecko: reach every Gecko-family target (firefox, gecko-based, and the forks waterfox, librewolf)
When a prefixed key matches the active target, Extension.js rewrites it to the unprefixed key in the emitted manifest. Fork targets inherit their engine family’s prefixes, so a manifest that only carries chromium:/firefox: keys still resolves correctly when you target a fork like brave or waterfox. An exact browser-name prefix also matches its own target (for example, brave: when you run --browser=brave).

For Chromium-based browsers (Chrome, Edge, …)

For Firefox

This makes service_worker available only for Chromium-family outputs while keeping background.scripts for Firefox outputs. Supported prefix map: An exact browser-name prefix (for example, chrome:, edge:, brave:, or waterfox:) resolves only when you target that same browser. It wins over its family prefix, so chrome: beats chromium: when you build for chrome. Safari builds inherit the Chromium family, because the converter consumes a Chrome-shaped manifest. chromium: keys apply to Safari, but chrome: and edge: keys do not. Use safari: (or webkit:) for Safari-only overrides. They take precedence over chromium: keys. Keys and permissions that Safari does not implement are dropped from the Safari build automatically. From 4.1.20, the build prints one line per dropped key, naming the key and why it went. content_scripts[].world is kept, because Safari has supported it since Safari 18. See Building Safari extensions. This works for any manifest field at any level, including permissions, content_scripts, and background.

Target one Chromium vendor

chromium: is the family prefix. chrome: and edge: each name one browser, so a field can ship to one store and stay out of the other. For example, a Chrome Web Store key must not reach the Edge Add-ons package:
extension build --browser=chrome emits key. extension build --browser=edge leaves it out. Do not write edge:key. Edge Add-ons rejects any package whose manifest contains key at all, so the field has no use in an Edge build. Partner Center assigns the extension id instead. From 4.1.20, a production Edge build drops key and prints one line saying why. A development build keeps it, where a stable id is useful and no store is involved. The prefix matches the browser that you request with --browser, not the binary that launches. When Extension.js falls back to another browser binary, prefixes still resolve for the requested target.
In Extension.js 4.1.19, chrome: and edge: became exact browser prefixes. Up to 4.1.18, they reached every Chromium-family target. When a build drops a chrome: or edge: key that 4.1.18 applied, the build prints a warning that names the key. Rename the key to chromium: to keep the old reach.

Precedence: the three-tier lattice

When several keys set the same field, the winner is decided by tier, not by position in the file:
  1. A plain key is the base.
  2. A family prefix (chromium: on Chromium targets, firefox: and gecko: on Gecko targets) overrides the plain key.
  3. A specific prefix overrides both. Specific means that the prefix names the exact target, such as chrome: for a chrome build or brave: for a brave build. On Safari and webkit-based targets, safari: and webkit: are both specific.
Source order only breaks ties inside one tier. Consider:
Building for chrome emits "Chrome". chrome: names the exact target, so it sits in the specific tier and beats chromium:. Building for edge, chromium, or brave emits "Family", because chrome: does not apply to those targets. A tie needs two prefixes in the same tier, such as firefox: and gecko: on a Gecko fork like waterfox, or safari: and webkit: on a Safari target. The later key in source order wins. A matching prefixed key always overrides a plain key with the same name, regardless of where each appears in the file.

Prefixes resolve at every depth

The resolver walks the whole manifest tree, including arrays. A prefixed key inside a content_scripts entry, or inside any nested object, resolves by the same three-tier rule as a top-level key.

The same resolver drives entry discovery

Prefix resolution is not only about the emitted JSON. The same resolver runs before script and HTML entry discovery, so a firefox:background script or a prefixed page becomes a compiled entry only on matching targets.

Forks and *-based aliases

Family classification matches a known-fork list first, then falls back to a substring check. chrome, edge, brave, opera, vivaldi and yandex classify as Chromium-family by name, as does any other name containing chromium. firefox, waterfox and librewolf classify as Gecko-family by name, as does any other name containing gecko or firefox. That is why the chromium-based and gecko-based aliases, and arbitrary *-based names built on them, inherit their family’s prefixed keys.

A prefixed manifest_version needs a plain fallback

chrome: and edge: are exact prefixes, so a manifest_version scoped to one vendor leaves every other build without one. This manifest gives Firefox and Chrome a version and gives Edge none:
No browser loads a manifest without manifest_version. From 4.1.21 the build refuses that case with an error that names the dropped key, for example chrome:manifest_version applies only to Chrome builds, so the edge build has no manifest_version. Up to 4.1.20 the build wrote the manifest and the browser rejected it later. Write a plain manifest_version as the base, and prefix only the exception:
Use chromium:manifest_version when the whole Chromium family should share one value that differs from the plain key.

Best practices

  • Keep shared defaults unprefixed: Put common fields in regular manifest keys, then prefix only browser-specific differences.
  • Prefix only when behavior diverges: Use browser prefixes when runtime requirements differ.
  • Build per target in continuous integration (CI): Generate and verify each browser output (dist/<browser>) to catch compatibility regressions early.
  • Validate with MDN: Use MDN Web Docs to confirm support before adding browser-only settings.

Next steps