Skip to content

The Install Prompt

When a browser decides your PWA is installable (valid manifest + registered service worker + HTTPS), it fires the beforeinstallprompt event on window. By default the browser would show its own install badge or prompt immediately — but you can intercept that event, hide the default UI, and show your own install button at the right moment.

let deferredPrompt = null;
window.addEventListener('beforeinstallprompt', (event) => {
// Prevent the browser from showing its mini-infobar
event.preventDefault();
// Save the event so we can trigger it later
deferredPrompt = event;
// Show your own install button
document.getElementById('install-btn').hidden = false;
});

Step 2 — trigger the prompt from a user gesture

Section titled “Step 2 — trigger the prompt from a user gesture”

You must call deferredPrompt.prompt() from inside a user-initiated handler (a click). Calling it outside a user gesture will throw.

document.getElementById('install-btn').addEventListener('click', async () => {
if (!deferredPrompt) return;
// Show the browser's install dialog
deferredPrompt.prompt();
// Wait for the user's choice
const { outcome } = await deferredPrompt.userChoice;
console.log('User choice:', outcome); // 'accepted' or 'dismissed'
deferredPrompt = null;
document.getElementById('install-btn').hidden = true;
});

The appinstalled event fires after the user confirms the install:

window.addEventListener('appinstalled', () => {
console.log('PWA installed successfully');
deferredPrompt = null;
});

Safari on iOS does not fire beforeinstallprompt. There is no programmatic way to trigger the install dialog. Your only option is to show an instructional banner that guides the user manually:

  1. Tap the Share button in Safari.
  2. Scroll down and tap Add to Home Screen.

Detect iOS to show the right guidance:

const isIos = /iphone|ipad|ipod/i.test(navigator.userAgent);
const isInStandaloneMode = window.matchMedia('(display-mode: standalone)').matches;
if (isIos && !isInStandaloneMode) {
document.getElementById('ios-hint').hidden = false;
}

The playground below wires up a complete install-button flow. Because StackBlitz runs inside an iframe, the beforeinstallprompt event will not fire — but you can inspect the full code and run it on a real domain to test.

Runs a real service worker + manifest in your browser.
OptionBenefitCost
Intercept beforeinstallprompt and show your own buttonFull control over timing and UI, does not interrupt the user when they are not readyYou own the state and event lifecycle yourself, adding code complexity
Let the browser show its own default install UINo extra code, standard UI immediatelyNo control over timing — it can appear before the user is ready to decide
  • Calling prompt() outside a user-gesture handler (e.g. inside a React useEffect on mount), which the browser rejects with an error.
  • Not stashing deferredPrompt from the very first event, missing the chance to ever show the install button if you were not listening in time.
  • Forgetting that iOS Safari never fires beforeinstallprompt at all, and shipping an install UI that simply does not work for iOS users.

Both beforeinstallprompt and deferredPrompt.prompt() are Chromium-only APIs (Chrome, Edge, Samsung Internet) — Firefox and Safari do not implement them (MDN: making PWAs installable). Waiting to show a custom install button until after the user has some engagement with the app — rather than on the very first page load — is itself a documented best practice for improving how often users accept it, independent of which specific app does it.

What must you call on the beforeinstallprompt event to suppress the browser default install UI?
From where must you call deferredPrompt.prompt()?
Which event fires after the user successfully installs the PWA?
How must iOS users install a PWA when beforeinstallprompt is not available?