Skip to content

Make it installable

Published

At the end of this guide the app passes Chrome’s install criteria, shows its own install button only on browsers that can honour it, offers iOS users the Share-sheet route instead of a dead button, and knows at runtime whether it is running installed. Start from a site that already meets the floor in Getting started; this guide builds the install experience on top of it.

Clear Chrome’s full criteria, including engagement

Section titled “Clear Chrome’s full criteria, including engagement”

web.dev’s install-criteria article lists what Chrome checks before firing beforeinstallprompt: the app is not already installed, the user has clicked or tapped the page at least once and spent at least 30 seconds on it (at any time, including a previous visit), the page is served over HTTPS, and the manifest has name or short_name, 192 px and 512 px icons, start_url, a display of fullscreen, standalone, minimal-ui, or window-controls-overlay, and no prefer_related_applications: true. The engagement rule is the one most often mistaken for a bug: on a fresh profile the event does not fire during the first half minute. Set scope so that every route the app owns stays inside the installed window; a URL outside scope opens with browser UI around it.

Capture the prompt and show your own button

Section titled “Capture the prompt and show your own button”

In Chromium browsers (Chrome, Edge, Samsung Internet) the browser fires beforeinstallprompt once the criteria hold. Call preventDefault() to suppress the browser’s own banner on mobile, keep the event, and reveal a button. prompt() resolves with outcome set to "accepted" or "dismissed" (MDN, BeforeInstallPromptEvent.userChoice), and a deferred event can be prompted only once:

let deferredPrompt = null;
const installButton = document.querySelector('#install');
window.addEventListener('beforeinstallprompt', (event) => {
event.preventDefault();
deferredPrompt = event;
installButton.hidden = false;
});
installButton.addEventListener('click', async () => {
if (!deferredPrompt) return;
const { outcome } = await deferredPrompt.prompt();
deferredPrompt = null;
installButton.hidden = true;
if (outcome === 'dismissed') {
localStorage.setItem('install-dismissed-at', String(Date.now()));
}
});
window.addEventListener('appinstalled', () => {
installButton.hidden = true;
deferredPrompt = null;
});

The button starts hidden and stays hidden on Firefox and Safari, which do not fire the event (MDN lists the event as not Baseline). The appinstalled listener also hides the button when the user installs through the browser menu rather than your button. Show the button after an action that signals intent, such as saving a second item; a prompt on first paint asks for a commitment before the app has shown any value, and a dismissal records a timestamp you can use to wait a week before asking again.

Give iOS users the Share-sheet instructions

Section titled “Give iOS users the Share-sheet instructions”

Safari on iOS and iPadOS has no beforeinstallprompt; the route is the Share button and then Add to Home Screen (English UI), as WebKit’s Web Push announcement describes. MDN notes that from iOS 16.4 the same Share-menu entry exists in Chrome, Edge, Firefox, and Orion on iOS. Detect the platform and the standalone state, then show instructions instead of a button:

const isIos = /iphone|ipad|ipod/i.test(navigator.userAgent) && !window.MSStream;
const isStandalone =
window.matchMedia('(display-mode: standalone)').matches || navigator.standalone === true;
if (isIos && !isStandalone) {
document.querySelector('#ios-install-hint').hidden = false; // "Tap Share, then Add to Home Screen"
}

navigator.standalone is the Safari-only property for a Home Screen web app; the display-mode media query is the cross-browser check and is what web.dev’s customize-install article uses for its getPWADisplayMode() helper.

Desktop Chrome shows an install icon at the right of the address bar on a page that meets the criteria; the menu path is More > Cast, save, and share > Install page as app… (English UI, Chrome help). On Android the path is More > Install and create shortcut > Install. After installing, launch the app and confirm that matchMedia('(display-mode: standalone)').matches is true in the console and that the install button is hidden. Uninstall from the installed window via More > Uninstall [app name] > Remove to retest the flow from a clean state.

← Back to the Guides overview.