The Install Prompt
The install flow
Section titled “The install flow”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.
Step 1 — stash the event
Section titled “Step 1 — stash the event”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;});Step 3 — react to a completed install
Section titled “Step 3 — react to a completed install”The appinstalled event fires after the user confirms the install:
window.addEventListener('appinstalled', () => { console.log('PWA installed successfully'); deferredPrompt = null;});iOS differences
Section titled “iOS differences”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:
- Tap the Share button in Safari.
- 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;}Try it live
Section titled “Try it live”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.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
Intercept beforeinstallprompt and show your own button | Full control over timing and UI, does not interrupt the user when they are not ready | You own the state and event lifecycle yourself, adding code complexity |
| Let the browser show its own default install UI | No extra code, standard UI immediately | No control over timing — it can appear before the user is ready to decide |
Common mistakes
Section titled “Common mistakes”- Calling
prompt()outside a user-gesture handler (e.g. inside a ReactuseEffecton mount), which the browser rejects with an error. - Not stashing
deferredPromptfrom 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
beforeinstallpromptat 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.