Skip to content

Install prompt UX: when and how to surface your install button

Published

In one line: Capture beforeinstallprompt, wait for a meaningful user signal, then call prompt() from a click handler — a cold install modal on first paint is almost always dismissed and wastes your only chance.

let deferredPrompt = null;
// 1. Save the event; suppress the mini-infobar.
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
showInstallAffordance(); // reveal your own button or banner
});
// 2. Trigger from a user gesture only.
installButton.addEventListener('click', async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
deferredPrompt = null; // event is single-use
hideInstallAffordance();
if (outcome === 'accepted') recordInstall();
});
// 3. Clean up when the browser completes the install.
window.addEventListener('appinstalled', () => {
deferredPrompt = null;
hideInstallAffordance();
});

Key rules:

  • e.preventDefault() is the only way to stop the mini-infobar.
  • prompt() must be called inside a user-activation context (click, touch). Calling it outside a gesture is silently ignored.
  • The saved event is single-use: once userChoice resolves, the same event object cannot call prompt() again. Clear the reference and re-listen if you need to re-prompt.

beforeinstallprompt fires after the browser’s engagement heuristics are satisfied. Chrome may also show a mini-infobar at the bottom of the viewport — preventDefault() blocks it. A custom in-page prompt then lets you choose the moment. After the user accepts, Chrome installs a WebAPK (not a shortcut) and fires appinstalled.

beforeinstallprompt does not fire on Safari (iOS or macOS). There is no programmatic install API. The only install path is: user taps the Share icon → “Add to Home Screen.” Your UX responsibility is to instruct the user with a visual hint (an arrow pointing at the Share icon or a custom banner) rather than trigger the dialog yourself. Detect whether the app is already installed with window.navigator.standalone === true to avoid showing the hint to users who have already added the app.

beforeinstallprompt fires on Chrome and Edge for Windows, macOS, Linux, and ChromeOS. The install experience opens a dialog window rather than a bottom sheet. The mini-infobar equivalent is a small install icon in the address bar. A custom prompt() call replaces that with your own flow. appinstalled fires after the install completes.

Firefox for Android supports installation (Firefox 128+) but does not fire beforeinstallprompt. There is no JavaScript install API; the user initiates from the browser menu. Desktop Firefox does not support PWA installation.

Signal Recommended action
beforeinstallprompt has fired Show a subtle install affordance (button or banner) — never a blocking modal
First page load, no engagement Do nothing yet; a cold prompt has extremely low acceptance
User completed a key task (checkout, article read, form saved) Surface the install button prominently
User opened the app multiple times in a week Inline header chip or persistent footer bar
Already installed (display-mode: standalone) Hide all install UI
iOS / Safari Show a “tap Share → Add to Home Screen” hint after 2–3 sessions
  • Blocking modals on first load. Chrome’s own research shows cold prompts are almost always dismissed.
  • Re-prompting immediately after a dismissal. Browsers may stop firing beforeinstallprompt after repeated dismissals; treat each prompt as precious.
  • Hiding the prompt behind obscure navigation. Make the affordance visible but not intrusive.
  • Forgetting iOS users. beforeinstallprompt absence does not mean iOS users cannot install; it means you must educate them differently.
const isStandalone =
window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone === true; // Safari iOS only
if (isStandalone) hideInstallAffordance();

Browser support for beforeinstallprompt varies. See /compatibility/ for current figures.

  • beforeinstallprompt is captured with preventDefault() and stored.
  • Install button is shown only after the event fires.
  • prompt() is called inside a click handler, not at load time.
  • Deferred event is cleared after userChoice resolves.
  • appinstalled listener hides the install UI and records the conversion.
  • Standalone detection suppresses install UI for already-installed users.
  • iOS fallback instructions are provided (Share → Add to Home Screen).