Skip to content

scope_extensions: extending a PWA's scope across origins

In one line: The scope_extensions manifest member “is used to extend the scope of a web app to include other origins, allowing multiple domains to be presented as a single web app,” per MDN, which flags it as experimental technology.

MDN lists the typical multi-origin situations this targets: different subdomains for content or functionality (support.example.com, shop.example.com), different subdomains for language or locale (uk.example.com, de.example.com), and related independent domains, such as a partner site. The WICG explainer describes why this matters for navigation: in Chromium browsers, an installed app window shows an out-of-scope UI bar when it navigates outside the manifest scope, and the explainer notes other implementations may instead open a new tab or browsing context in a regular browser window — behavior the explainer says a developer may not want for first-party content that just happens to live on another origin.

Per MDN, the main app lists the origins it wants to include, each as an object with a type and an origin:

{
"scope_extensions": [
{ "type": "origin", "origin": "https://support.example.com" },
{ "type": "origin", "origin": "https://shop.example.com" }
]
}

Per MDN, type is “a string defining the type of scope extension. This is currently always origin,” and origin is “a string representing an origin that the web app wishes to extend its scope to.”

Listing an origin in the manifest is only half the handshake. Each extended origin must also host .well-known/web-app-origin-association, confirming it accepts the association back to the app. Sources do not agree on one settled JSON shape for this file yet. The WICG explainer’s “Related Proposals” section, describing how this differs from the earlier url_handlers proposal, shows entries keyed by the web app’s identifier with a scope value:

{ "https://example.com/": { "scope": "/foo" } }

Chrome’s developer documentation, however, currently shows a different shape — a web_apps array of objects keyed by web_app_identity:

{ "web_apps": [{ "web_app_identity": "https://example.com/" }] }

Check the live WICG explainer and your target engine’s current implementation before hard-coding either shape into a production association file.

The WICG explainer describes scope_extensions only as a manifest member and an origin-association file read by the browser; it defines no companion JavaScript property or runtime API for reading back whether a navigation was honored as in-scope. Checking display-mode does not detect scope_extensions support — per MDN, display-mode only reports which display mode (standalone, minimal-ui, window-controls-overlay, fullscreen, picture-in-picture, or the default browser) a page is currently rendered in, and MDN’s display-mode documentation does not mention scope_extensions or which origins it covers.

Because the explainer defines no runtime detection mechanism for scope_extensions, link to an associated origin the same way regardless of whether the browser honors scope_extensions, and let the browser apply its own outcome. Per the WICG explainer, Chromium browsers show an out-of-scope UI bar when the navigation lands outside the manifest scope, and other implementations may instead open a new tab or browsing context — both are the browser’s own handling and require no application code:

function openRelatedOrigin(url) {
location.assign(url);
}

Use display-mode only to adjust unrelated presentation details, never to change this navigation logic:

const installedModes = ["standalone", "minimal-ui", "window-controls-overlay"];
const looksInstalled =
window.matchMedia &&
installedModes.some((mode) => window.matchMedia(`(display-mode: ${mode})`).matches);
if (!looksInstalled) {
// Rendered in browser, fullscreen, or picture-in-picture, not one of the
// installed-style modes. This says nothing about scope_extensions
// support — it only adjusts presentation, e.g. showing a hint that a
// related-origin link may open a new tab:
document.querySelectorAll("[data-related-origin-link]").forEach((el) => {
el.title = "Opens a related site; it may open in a new tab.";
});
}

Design the UI copy around the associated-origin link so a user is not confused if, per the behavior the WICG explainer describes for Chromium and some other implementations, that navigation lands on a chrome-visible page or a new tab instead of staying inside the app-like window.

MDN marks scope_extensions as experimental and advises checking its browser-compatibility table before production use.

  • Treat scope_extensions as experimental — MDN explicitly flags it and tells readers to check the compatibility table before shipping it in production.
  • Host .well-known/web-app-origin-association on every origin listed in scope_extensions — the association is a two-way handshake, and a missing or malformed file on the extended origin means the extension does not take effect for that origin.
  • Do not assume the association file’s format is fixed — the WICG explainer shows one shape, Chrome’s developer documentation currently shows another; check both before shipping.
  • Design any cross-origin link so its UI copy still makes sense on engines that ignore scope_extensions — the WICG explainer describes Chromium’s un-extended result as an out-of-scope bar, and notes some other implementations may open a new tab instead.
  • Do not treat a display-mode check as scope_extensions support detection — the WICG explainer defines no JavaScript API for confirming whether the browser honored scope_extensions, and MDN’s display-mode documentation does not mention scope_extensions. Keep the associated-origin navigation working the same way regardless.
  • When using the WICG explainer’s keyed-by-identity association shape, use its scope value to restrict an extended origin to only the paths it actually wants covered, rather than its whole origin.