Web Platform · Progressive Web

Web App Manifest and the Installability Contract

How the manifest establishes an installed application identity through id, start_url, and scope, what display modes change at launch, and why installability is a browser policy rather than a specification rule.

A manifest is a JSON resource linked from a document with <link rel="manifest">. The browser fetches it during page processing, parses it with its own set of rules, and keeps the result. No script reads it to apply it: by the time a page’s JavaScript runs, the manifest has already influenced how the page may be installed and how an installed instance will launch. That separation is why manifest mistakes are invisible in the page and only show up in the installation offer or in the window chrome after installation.

Identity is id plus start_url

An installed web application is identified by the manifest’s id member, resolved relative to the manifest’s own URL. If id is absent, the browser uses start_url as the identity. This matters across deployments: changing start_url from /app/ to /app/home/ without setting id produces a different identity, so an existing installation is not updated — a second installable app appears alongside the first, and the old one keeps launching the old URL until it is removed.

{
  "id": "/app/",
  "name": "Field Notes",
  "short_name": "Notes",
  "start_url": "/app/today",
  "scope": "/app/",
  "display": "standalone"
}

start_url must be same-origin with the document that links the manifest, and it is resolved against the manifest URL. scope is the navigation boundary: an installed instance is considered to be within its app when the URL falls under that prefix, and links outside it open in a browser tab instead of the app window. If scope is omitted, it defaults to the directory of start_url; a start_url of /app/today therefore yields a scope of /app/, and links to /app/settings stay inside the app while /admin/ does not. A start_url outside the declared scope causes that scope member to be ignored, and the navigation boundary falls back to the parent path of start_url.

The manifest’s navigation scope is independent of the service worker’s registration scope. A manifest scope of /app/ with a worker registered at /sw.js gives the worker the whole origin; the manifest scope only restricts what counts as in-app navigation. Aligning them keeps the offline behaviour and the installed window consistent, which is why the service worker lifecycle and the manifest are usually configured from one routing decision. Launching an installed instance is an ordinary navigation to start_url, and the handler that answers it when the network is unavailable is described in Offline Navigation, App Shell, and Fallback Routing.

Display modes and what the window becomes

display selects the amount of browser chrome an installed instance gets. The commonly implemented values, from most to least chrome, are browser, minimal-ui, standalone, and fullscreen. standalone removes the address bar and leaves a system title bar or window controls; minimal-ui keeps a reduced set of navigation controls; fullscreen hides browser UI and uses the whole display area, so an application that depends on a visible exit control should provide one of its own. window-controls-overlay is a newer value that keeps the title bar area but lets the page draw into it.

display_override is a fallback list evaluated before display. An array of ["window-controls-overlay", "standalone"] asks for the overlay where supported and degrades to standalone elsewhere. The fallback chain ends at browser, so an unsupported mode degrades to a tab; a manifest whose display resolves to browser generally does not meet the installability criteria of browsers that require a non-browser display mode.

The selected mode is observable from CSS through the display-mode media feature and from script through matchMedia('(display-mode: standalone)'). Both report the mode of the current window, not the manifest’s preference, which is what makes them usable for adjusting layout in an installed instance. A page that hides a download banner based on that query must also handle the case where the same page is open in a tab and in an installed window at the same time.

Icons: purposes and the maskable safe zone

icons is an array of objects with src, sizes, type, and purpose. sizes is a hint in the form "192x192" or "any" for vector formats, and browsers choose the entry that best fits the required display size. Providing several raster sizes reduces resampling artifacts on high-density displays, and an SVG with "sizes": "any" covers the range without a size ladder, at the cost of relying on the platform’s SVG support in the installer.

purpose changes how the image is treated. "any" is the default and assumes the icon can be displayed as-is. "maskable" declares that the image is designed to be cropped to an arbitrary shape, with all meaningful content inside a circular safe zone whose diameter is 80 percent of the image — a 10 percent margin on each side that the platform may discard. "monochrome" declares a single-color image that the platform may tint. An icon marked maskable but drawn to the edges loses its outer ring on launchers that apply a circular mask, and an icon not marked maskable may be letterboxed inside a shape instead.

The practical arrangement is two entries at the same size: one any with the full-bleed artwork and one maskable with the content pulled into the safe zone. The browser picks by purpose and size, so both are available without a negotiation step.

Colors, splash, and the first paint

background_color and theme_color are the two members that shape the launch experience without involving script. The background color is painted behind the page while the document loads, and on platforms that generate a launch screen it is the fill color of that screen, combined with the app name and the largest icon the manifest supplies. A light icon set with a dark background color therefore produces a splash with a dark field and a bright glyph, and no stylesheet can alter it, because the screen is drawn before any document exists.

theme_color tints the browser interface: the address bar on some platforms, the status bar on others, and the title bar of a standalone window. It can be overridden per document by a theme-color meta element, which is the only way to change it at runtime; the manifest value is a default. Neither color participates in contrast calculation, so a manifest whose theme color matches its background makes window controls disappear against the frame on platforms that draw them over the page.

Installability is policy, not specification

The manifest specification defines parsing and processing. It does not define when a browser offers installation. That decision belongs to each browser and changes between releases: transport security, a manifest that parses and supplies a name, a start_url within scope, an icon of a sufficient size, a display mode other than browser, and often a service worker with a fetch handler are the recurring ingredients. Which combination suffices is a matter of the browser’s current policy, and a manifest that satisfies one browser’s criteria may be installable in neither of the others.

This has a design consequence. An interface that depends on the installation offer must degrade to nothing when the offer never appears, and there is no standard event that reports “this browser considers the page installable”. The beforeinstallprompt event is a Chromium extension, not part of the manifest specification; it fires only when that browser has decided the criteria are met, and it can be deferred, stored, and triggered later by prompt(). The returned promise resolves to a userChoice object whose outcome is "accepted" or "dismissed", and the event may be used once — a second prompt() call on the same event rejects.

let deferred = null;

window.addEventListener('beforeinstallprompt', (event) => {
  event.preventDefault();     // Suppress the automatic mini-infobar.
  deferred = event;
});

async function requestInstall() {
  if (!deferred) return null;              // No install event captured in this browser.
  const { outcome } = await deferred.prompt();
  deferred = null;                          // The event is single-use.
  return outcome;
}

Browsers that do not implement the event still install from their own menu, so the manifest remains the only cross-browser mechanism for controlling what an installed instance looks like.

Manifest parsing and update rules

The manifest is fetched with credentials for same-origin requests by default; a cross-origin manifest is fetched without credentials unless the link element carries crossorigin="use-credentials" and the response permits it. The response should be served as application/manifest+json, though application/json is also accepted by common implementations. Parse errors do not fail the whole document: unknown members are ignored, invalid members are dropped, and the manifest is processed with the valid remainder. A single malformed icon entry does not invalidate a correct start_url, but a manifest with neither name nor short_name, or with a start_url that fails the same-origin check, leaves nothing installable.

Manifests are re-fetched when the document loads, not on a timer, and the copy the browser holds is updated when a new one is processed. Because the identity is id and not the manifest URL, moving the manifest does not change an absolute id; a relative id is resolved against the manifest URL, so moving the file changes the identity in that case. Changing id itself always produces a different application. Shortcut entries, screenshots used by richer install dialogs, and share_target registrations are all declarative additions to an installed app, each with its own validation, and each silently ignored when invalid. The only reliable way to know what a browser accepted is the browser’s own application panel or devtools manifest view, since the page cannot read back the processed manifest through a standard API.

References

  1. Web Application Manifest — W3C Web Applications Working Group
  2. Web app manifests — MDN Web Docs