Skip to content

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.

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.

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.

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.

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.

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.

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.

Specifications

SpecificationStatus
Web Install: the navigator.install() APIWICG draft
Web Install: common request preconditionsWICG draft
Web Install: permission and user consentWICG draft