JavaScript PWA Install Table

PieceWhat it doesField note
beforeinstallprompt eventThe install momentFires when installable - capture it, DO NOT show a banner instantly
e.preventDefault()Own the promptStops the mini-infobar - your button triggers e.prompt() later
userChoice.outcomeaccepted / dismissedReport it - and DISMISSED MEANS STOP ASKING for a while
getInstalledRelatedAppsAlready installed?Detect the native app - do not offer installing the web twin
Requires manifest+SW+HTTPSThe eligibilityManifest with icons+name, service worker, secure context
appinstalled eventPost-installHide the button, maybe show a welcome - installed is a state
iOS: no prompt eventThe platform gapiOS = share-sheet instructions - detect platform, show how-to
Never on loadThe ask disciplineAsk after value demonstrated - the notifications law again
Reference: the MDN beforeinstallprompt reference. The install flow done right: capture beforeinstallprompt and preventDefault - suppressing the browser's mini-infobar - then fire e.prompt() from YOUR button after the user has seen the app's value. userChoice reports accepted/dismissed, and DISMISSED MEANS STOP: asking again next load is the fastest route to users ignoring or leaving. Bottom line: eligibility is manifest + service worker + HTTPS, iOS has no prompt event (share-sheet instructions instead, platform-detected), and appinstalled hides your button - installed is a state, not a milestone to re-celebrate. Related tools: service worker table (the eligibility half), notifications table (the same ask-discipline), and permissions table (the state reading).

The install flow done right: capture beforeinstallprompt and preventDefault - suppressing the browser's mini-infobar - then fire e.prompt() from YOUR button after the user has seen the app's value. userChoice reports accepted or dismissed.

Bottom line: DISMISSED MEANS STOP - re-asking on every load is the fastest route to users ignoring installs or leaving. Eligibility is manifest (icons, name, start_url) + service worker + HTTPS, iOS has no prompt event (share-sheet instructions instead, platform-detected), and appinstalled hides your button - installed is a state, not a milestone.

The honest part: install prompts are the loudest ask a site can make, and they compete with notifications for the same trust budget. The discipline that works: one well-timed ask, ever; a dismissible state that lasts weeks; and an app good enough that users go looking for the install button themselves.

How to use

  1. Capture and hold: window.addEventListener('beforeinstallprompt', e => { e.preventDefault(); deferredPrompt = e; showOwnButton(); }) - the browser's banner never appears.
  2. Fire from your button: deferredPrompt.prompt(); const { outcome } = await deferredPrompt.userChoice - accepted hides the button; dismissed starts the cool-down.
  3. Handle the iOS gap: detect iOS Safari and show a one-time instruction ('Share, then Add to Home Screen') - there is no prompt event to capture there.

Frequently asked questions

Why capture beforeinstallprompt instead of letting the browser show it?

Timing and placement. The default mini-infobar appears as soon as eligibility is met - often seconds after first visit, before the user has any reason to install. Capturing it (preventDefault) moves the ask to YOUR moment: after the user completed a task, hit a usage threshold, or clicked a persistent 'Install app' button. The data across the industry is consistent: install prompts shown contextually after value convert far better than banners shown on arrival - and they protect the trust budget that notifications will want later. Owning the prompt is owning the ask.

What does 'dismissed means stop' mean in practice?

The user answered the question. userChoice.outcome === 'dismissed' is not a failed conversion to retry next load - it is a decision, and re-prompting every session trains users to ignore or resent the site (and some browsers eventually stop firing the event for nagging sites). The working discipline: store the dismissal with a timestamp (localStorage), cool down for weeks or forever, and never nag within a session. If the install matters, make the button permanently available but quiet - users who want it will click a visible 'Install' button; nobody wants to be asked what they already declined.

What makes a PWA eligible for the install prompt at all?

Three pillars, all verifiable: a web app manifest with name, icons (192px and 512px minimum), start_url and display mode; a registered service worker with a fetch handler (the offline-capability signal); and HTTPS. Miss any and beforeinstallprompt never fires - the silent failure that confuses teams ('the event just never happens'). The debugging order: devtools Application panel checks the manifest errors, confirms the service worker is activated, and verifies the HTTPS origin. iOS skips this whole machinery - its 'Add to Home Screen' works from the share sheet regardless, without install prompts at all.

What should change after appinstalled fires?

The install UI retires and the experience adapts. Hide the install button (installed is a state - re-celebrating it reads as a bug), and consider a one-time welcome-for-installed flow: the app now launches standalone (display: standalone in the manifest), possibly with more screen real estate and no browser UI. Use display-mode: standalone in CSS or matchMedia to adjust navigation (hide the 'you are in a browser' hints). The one thing NOT to do: prompt again or show 'install our app' messages to a user running the installed app - detecting the standalone display mode and suppressing install UI is the polish that separates maintained PWAs from set-and-forget ones.

Related tools