Skip to content

Manifest · Manifest member

Manifest scope: your PWA's navigation boundary

Published Updated

Limited availabilityNot supported in Firefox (Desktop)W3C

In one line: Per the Web Application Manifest specification, scope “is a string that represents the navigation scope of this web application’s application context”; per MDN it specifies “the top-level URL path that contains your web application’s pages and subdirectories”, pages within it “provide an app-like interface”, and outside it browsers “display UI elements like the URL bar to indicate the change in context”.

scope is a string holding a URL — absolute, or relative and resolved against the manifest file’s URL (MDN). The safe shape is an explicit scope that start_url sits inside:

{
"name": "My Hiking Web App",
"start_url": "https://hikingapp.com/store/",
"scope": "https://hikingapp.com/store/"
}

With that setup, per MDN’s own example, “pages like https://hikingapp.com/store/products are part of your web app, but https://hikingapp.com/company/ is out of your web app’s scope.”

  • In scope. Per MDN, “a URL is considered to be ‘within scope’ if its path begins with the URL path defined in scope” — with scope set to /app/, the URLs /app/, /app/page.html and /app/dashboard/index.html are all within scope, while / and /page.html are not.
  • Out of scope. The app-like interface broadly remains, but “browsers display additional UI elements like the URL bar”, which “helps users understand that they’re viewing pages outside the app’s defined scope”.
  • Scope is not a wall. MDN is explicit: “The scope member doesn’t prevent users from navigating to app pages outside of the defined scope. Off-scope navigations are not blocked by browsers.” How an out-of-scope document is presented is up to the user agent; what you can rely on is that it is not blocked.

The specification states the matching rule exactly: a URL target is within scope of a URL scope if the two are same origin and target’s path starts with scope’s path.

Per MDN, “string matching for the scope URL uses a simple prefix match, not the path structure”: a scope of /prefix “will match URLs starting with /prefix, including /prefix-of/index.html and /prefix/index.html”. That is why MDN recommends “to define a scope ending with a /” — setting /prefix/ “ensures it will match only the pages within the /prefix/ directory”.

scope is optional, and an invalid one is discarded rather than honoured. Per MDN, if scope is not specified or the value is invalid — “not a string, not a valid URL, or start_url is not within the specified scope” — “the effective scope will be set to the start_url value after removing its filename, query, and fragment”. The specification describes the same default: the start URL, “but with its filename, query, and fragment removed”.

MDN’s worked examples:

start_url Effective scope when scope is missing or invalid
https://example.com/app/index.html?user=123#home https://example.com/app/
/pages/welcome.html /pages/ on the same origin
/pages/ (the trailing slash is important) /pages/

MDN’s advice is not to lean on it: “To avoid issues with scope determination in this way, it’s recommended to explicitly specify scope in your manifest file.”

To check a URL against your own scope, read the declared value out of the manifest and apply the specification’s within-scope rule. The helper returns null when it has no usable declared scope to report — that is not an assertion that a URL is out of scope, only that it could not answer.

// The specification's within-scope rule: same origin, and the target's path
// starts with the scope's path.
function isWithinScope(target, scope) {
if (!scope) return false;
const targetUrl = new URL(target, location.href);
const scopeUrl = new URL(scope, location.href);
if (targetUrl.origin !== scopeUrl.origin) return false;
return targetUrl.pathname.startsWith(scopeUrl.pathname);
}
// Reads the DECLARED scope. It does not reproduce the browser's own fallback:
// when `scope` is absent or invalid, the browser derives an effective scope
// from `start_url` instead.
async function declaredScope() {
const link = document.querySelector('link[rel="manifest"]');
if (!link || !('fetch' in window)) {
// Fallback: no manifest to read — treat the scope as unknown.
return null;
}
try {
const manifest = await fetch(link.href).then((response) => response.json());
if (typeof manifest.scope !== 'string' || manifest.scope === '') return null;
const scope = new URL(manifest.scope, link.href).href;
// MDN: an undefined or invalid `start_url` defaults to the page that links
// to the manifest; and the scope is invalid if `start_url` is not within it.
let startUrl = location.href;
if (typeof manifest.start_url === 'string' && manifest.start_url !== '') {
try {
const parsed = new URL(manifest.start_url, link.href);
if (parsed.origin === location.origin) startUrl = parsed.href;
} catch {
// Keep the document URL, the way the browser would.
}
}
return isWithinScope(startUrl, scope) ? scope : null;
} catch {
return null; // Unreachable manifest, unparseable JSON, or an invalid URL.
}
}

Per MDN, “other applications can deep link directly to specific pages of your web app. The scope member affects how these deep-linked pages are displayed, but it is not required for deep linking to work.” A shared in-scope link opens “in the app’s interface without browser controls”; an out-of-scope one opens in the app-like interface “but with the website address and browser controls visible”.

On Android the same string drives link capture. Per web.dev, an installed PWA “will register a set of intent filters for all URLs within the scope of the app”, and scope “tells Android to only open your web app if the URL matches the origin + scope”, which “gives you control over which URLs will be handled by your app, and which should be opened in the browser”. With "scope": "/app/", web.dev’s example captures https://example.com/app/read/book but not https://example.com/help/.

  • End the scope with / — /prefix also matches /prefix-of/index.html (MDN).
  • Keep start_url inside scope. If it is not, the scope is invalid and the browser falls back to the start_url-derived default, silently ignoring what you wrote.
  • Declare scope explicitly rather than relying on the fallback, as MDN recommends.
  • Remember a relative scope resolves against the manifest’s URL, not the page’s.
  • Do not treat scope as a navigation control: per MDN, “off-scope navigations are not blocked by browsers”.
  • On Android, scope is what becomes intent filters — anything outside it opens in the browser rather than the installed app (web.dev).
  • Test a shared in-scope deep link on an installed app and confirm browser controls stay hidden.

Specifications

SpecificationStatus
Web App Manifest: scopeW3C
  • Legend
  • Yes
  • Partial
  • Flag
  • No
  • Unknown
Browser / PlatformSupportVersionsConfidenceSourceNotes
Chrome (Android)Yes73mediumsource—
Chrome (Desktop)Yes73mediumsource—
Edge (Desktop)Yes79mediumsource—
Safari (iOS)Yes16.4mediumsource1
Safari (macOS)Yes17mediumsource2
Firefox (Desktop)No—mediumsource3
Samsung InternetYes6.2mediumsource—
  1. Honored for home-screen web apps; off-scope navigation surfaces a banner rather than a full address bar.
  2. Applies to Dock-installed web apps.
  3. No manifest-based install path, so scope is not applied.

Ecosystem & commercial policy

EntityTypeContextStatusSponsoredNotes
Google Play (TWA)distribution_channelGoogle Play TWAYesNoassetlinks.json origin must match the manifest scope origin, or the TWA degrades to a Custom Tab with browser chrome.
Microsoft Store (PWA)distribution_channelMicrosoft StoreYesNoScope defines the in-app boundary store reviewers preview at submission.

Source data: /compatibility/manifest-scope.json · Global usage: 90 % (StatCounter 2026-05)

Source: spec · MDN · Last verified 2026-06-24 · Confidence: medium (computed from sources)