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.
Verify each path
Section titled “Verify each path”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.
See also
Section titled “See also”- Getting started
- Install prompt
- iOS Add to Home Screen
- Install prompt compatibility
- How to provide your own in-app install experience (web.dev)
- Installation criteria (web.dev)
← Back to the Guides overview.