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

> Best-practice patterns for timing, placement, and copy of a custom PWA install prompt using the beforeinstallprompt API, including iOS and desktop fallbacks.

**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.

## The core event lifecycle

```js
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.

## Platform behavior

### Android (Chrome / Chromium)

`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`.

### iOS / Safari

`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.

### Desktop (Chrome / Edge)

`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

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.

## When to surface the install UI

| 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 |

## UX anti-patterns to avoid

- **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.

## Detecting already-installed state

```js
const isStandalone =
  window.matchMedia('(display-mode: standalone)').matches ||
  window.navigator.standalone === true; // Safari iOS only

if (isStandalone) hideInstallAffordance();
```

## Compatibility

Browser support for `beforeinstallprompt` varies. See [/compatibility/](/compatibility/)
for current figures.

## Practical checklist

- [ ] `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).