API in Installation
Web Install API (navigator.install)
Published
navigator.install() is a promise-based method that asks the browser to install a web app:
the manifest linked by the current document when it is called with no argument, or the manifest
at a URL the caller supplies. The explainer positions it as an extension of
beforeinstallprompt, which cannot install content other than the web application already
loaded.
Support position
Section titled “Support position”BCD api.Navigator.install records support from Chrome 154 behind a preference flag named
#web-app-installation-api set to Enabled, mirrored into the Chromium-based browsers the
record lists, and no support in Firefox or Safari. The record is marked experimental and
standard-track.
Chrome Platform Status feature 5183481574850560 (chromestatus.com) lists the Web Install API as enabled by default in Chrome 157, and summarises it as “Starting in Chrome 157, the new Web Install API allows websites to prompt users to install a web app, given a manifest URL and optional manifest ID.” The same page records the specification maturity as “Specification being incubated in a Community Group”, Mozilla’s standards position as “No signal”, and WebKit’s as “Oppose”, each linking the corresponding standards-positions issue.
Syntax
Section titled “Syntax”await navigator.install();await navigator.install({ manifest: 'https://suite.example/mail/manifest.json' });await navigator.install({ manifest: 'https://music.example/manifest.webmanifest', manifestId: 'https://music.example/app',});The draft specification declares two overloads on a partial Navigator interface, both returning
Promise<WebInstallResult>: one with no argument and one taking an InstallParams dictionary
whose manifest member is required and whose manifestId member is an optional, nullable
USVString. WebInstallResult is declared as an empty dictionary.
Parameters
Section titled “Parameters”The dictionary may be omitted entirely, in which case the manifest of the page already loaded is the install target.
| Parameter | Type | Meaning |
|---|---|---|
options.manifest |
USVString, required |
The URL of the application manifest identifying the target web application. Relative values are resolved against the initiating document’s URL. |
options.manifestId |
USVString?, optional |
An absolute manifest ID assertion: the processed manifest id the caller expects. The draft states that null and omission have the same meaning. |
The explainer states that the developer may omit manifestId if and only if the JSON at
manifest contains an id. Where a manifest declares no id member, the
Web App Manifest (w3.org) computes the id from
the start_url, and the explainer points at the Application tab of Chromium DevTools as the
place that computed id is readable.
The promise resolves with a WebInstallResult, which the draft defines as an empty dictionary
for future compatibility.
Exceptions
Section titled “Exceptions”The method is declared to return Promise<WebInstallResult>, so each failure below reaches the
caller as a promise rejection: handle them with await inside try/catch, or with a rejection
handler on the returned promise. The draft’s method steps use the specification’s throw wording
for the first three, which Web IDL turns into a rejected promise for a promise-returning
operation, and the explainer’s own steps describe those same names as names the promise rejects
with. The draft notes that user activation is consumed before argument validation, which matches
Chromium, so an unparseable URL still spends the activation.
| Rejection | Raised at | When |
|---|---|---|
InvalidStateError |
the method steps, before any request starts | The document is not an eligible initiating document. The draft requires a fully active document that is the active document of a top-level traversable, is not a fenced frame document, has an empty active sandboxing flag set, and has the web-app-installation policy-controlled feature enabled. |
NotAllowedError |
the method steps, before any request starts | The document has no transient activation. |
TypeError |
the method steps, before any request starts | options.manifest fails to parse against the document’s URL, or options.manifestId fails to parse as an absolute URL. |
DataError |
the request outcome | The outcome was invalid install data: per the draft, the supplied URL, manifest, identity assertion, or required installation metadata was invalid or unavailable. The explainer lists an invalid manifest URL, a failed manifest fetch, a malformed manifest, an omitted manifestId where the manifest declares no id, and a manifestId that does not match the computed one. |
AbortError |
the request outcome | Every other non-completion. The draft’s note states that “Permission denial, unsupported profiles, navigation, concurrency, user dismissal, and internal failure all map to AbortError.” |
The explainer also lists NotFoundError for a missing document among the names the promise can
reject with, while the draft’s provisional method steps route every ineligible initiating
document to InvalidStateError instead; the draft marks that taxonomy as unresolved.
One further rejection path has no name of its own: the draft gives each top-level traversable a single active web install request, so a second call made while a dialog is open finishes with installation aborted.
Examples
Section titled “Examples”Each example runs from a click handler, because the draft rejects with NotAllowedError without
transient activation. The dictionary form additionally goes through the web-app-installation
permission.
Installing the current page from a button
Section titled “Installing the current page from a button”The no-argument form targets the manifest the document links. In the draft’s
“obtain web install permission” steps a current-document source returns true without requesting
the web-app-installation permission, so the flow reaches the browser’s confirmation UI
directly. A manifest with no id member resolves one through start_url, but the explainer’s
no-argument install steps reject with DataError when the manifest file declares no id, so
declare one.
const button = document.querySelector('#install');
button.addEventListener('click', async () => { try { await navigator.install(); button.hidden = true; } catch (err) { if (err.name === 'DataError') { // Manifest missing, unfetchable, or declaring no `id`. reportInstallBlocked('manifest'); } else if (err.name === 'AbortError') { // Dismissed, denied by policy, or an unsupported profile. button.textContent = 'Install'; } }});Branch on err.name rather than on a message string: the names above are the ones the draft and
the explainer define, and the messages are not specified.
Installing a sibling app from a catalog page
Section titled “Installing a sibling app from a catalog page”The dictionary form names another app’s manifest. The explainer’s steps fetch that manifest with
credentials mode "omit", and its open questions record the current decision that the manifest
URL be same-origin with start_url, with relative URLs inside the manifest resolved against the
manifest URL. Pass manifestId when the target manifest declares no id of its own.
const CATALOG = [ { name: 'Mail', manifest: 'https://suite.example/mail/manifest.json' }, { name: 'Calendar', manifest: 'https://suite.example/calendar/manifest.json' },];
async function installFromCatalog(entry) { try { await navigator.install({ manifest: entry.manifest }); return { ok: true }; } catch (err) { if (err.name === 'TypeError' || err.name === 'DataError') { return { ok: false, reason: 'catalog-entry-stale' }; } return { ok: false, reason: 'not-installed' }; // AbortError and anything else }}A stale catalog entry is the case DataError exists for: the explainer’s open questions keep
versioned manifest filenames unsolved and rely on DataError to tell the caller a published URL
has broken.
Detecting support and falling back to beforeinstallprompt
Section titled “Detecting support and falling back to beforeinstallprompt”The permission query tells a page whether a cross-origin install would be refused before it
renders a button, and 'install' in navigator tells it whether the method exists at all. Where
it does not, the deferred beforeinstallprompt event is the Chromium path that does, and the
remaining browsers have their own install surfaces instead.
let deferredPrompt = null;addEventListener('beforeinstallprompt', (event) => { event.preventDefault(); deferredPrompt = event;});
async function installTarget(manifestUrl) { if (!('install' in navigator)) { if (deferredPrompt) return deferredPrompt.prompt(); // Chromium's older entry point return { ok: false, reason: 'use-browser-menu' }; // no scripted install here } const status = await navigator.permissions .query({ name: 'web-app-installation' }) .catch(() => null); // name unsupported: TypeError per the Permissions spec if (status?.state === 'denied') { return { ok: false, reason: 'permission-denied' }; } return navigator.install({ manifest: manifestUrl });}BCD api.Permissions.permission_web-app-installation records the permission name from Chrome 154
behind the same preference flag, so the catch branch covers the browsers where the name is not
registered and query() rejects with a TypeError.
See also
Section titled “See also”- Web Install (wicg.github.io)
- Web Install API explainer (github.com)
- beforeinstallprompt event
- Installability criteria
Specifications
| Specification | Status |
|---|---|
| Web Install: the navigator.install() API | WICG draft |
| Web Install: common request preconditions | WICG draft |
| Web Install: permission and user consent | WICG draft |