Chromium browsers fire a `beforeinstallprompt` event on `window`. How do you use it to drive your own "Install app" button, and what restrictions apply to calling `prompt()`?
answer
- preventDefault, then keep the object
- the button needs a saved handle
- gesture-bound, single use
- outcome is not the same as installed
- Chromium-only; iOS uses the share sheet
basics
~20 sListen for beforeinstallprompt, call preventDefault() to suppress the browser's own UI, and keep the event object. Show your install button, and inside a user gesture call the saved event's prompt(), then await userChoice. The event can be prompted only once.
solid answer
~40 sChromium fires `beforeinstallprompt` on `window` once it decides the site is installable. In the handler you call `event.preventDefault()` to stop the browser's built-in mini-infobar, stash the event object in a variable, and reveal your own install affordance. When the user clicks it you call `savedEvent.prompt()` — that call must happen in response to a real user gesture, and a given event object can only be prompted once. Then `await savedEvent.userChoice` resolves to `{ outcome: 'accepted' | 'dismissed' }`, after which you drop the reference and hide the button. A separate `appinstalled` event on `window` tells you the install completed. This is a Chromium-only API as of 2025: Safari and Firefox never fire it, so the button must be feature-detected and iOS users still install through the share sheet.
code
javascript · 23 lineslet deferredPrompt = null;
const installButton = document.querySelector('#install');
installButton.hidden = true;
window.addEventListener('beforeinstallprompt', (event) => {
event.preventDefault();
deferredPrompt = event;
installButton.hidden = window.matchMedia('(display-mode: standalone)').matches;
});
installButton.addEventListener('click', async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
deferredPrompt = null;
installButton.hidden = true;
console.log('install prompt outcome:', outcome);
});
window.addEventListener('appinstalled', () => {
deferredPrompt = null;
installButton.hidden = true;
});go deeper
Know that the browser, not your code, decides installability, and that the event exists so a site can show its own install button rather than relying on browser UI.
Walk the sequence precisely: preventDefault, retain the event, gesture-bound prompt(), await userChoice, discard. Say out loud that the object is single-use.
Discuss the product consequences: install rate cannot be measured uniformly across browsers, iOS needs an instructional path, and analytics should distinguish dismissal from the appinstalled event.
Own whether install is worth pursuing at all for the product — it fragments the surface you support, and the payoff is real only when re-engagement, offline use or push justify maintaining an installed experience alongside the web one.
## The problem the event solves Browsers decide on their own whether a site is installable, and their built-in install affordance is easy to miss — an address-bar icon on desktop, a small infobar on Android. `beforeinstallprompt` hands that decision to the page: the site can put an install button where it makes sense, at the moment it makes sense, and let the browser render the actual system dialog. ## The full sequence ```js let deferred = null; window.addEventListener('beforeinstallprompt', (event) => { event.preventDefault(); // suppress the browser's own mini-infobar deferred = event; // keep it; it is the only handle you get installButton.hidden = false; }); installButton.addEventListener('click', async () => { if (!deferred) return; deferred.prompt(); // must be inside a user gesture const { outcome } = await deferred.userChoice; deferred = null; // one prompt per event object installButton.hidden = true; analytics.track('install_prompt', { outcome }); }); window.addEventListener('appinstalled', () => { deferred = null; installButton.hidden = true; }); ``` Four rules fall out of that code and are what an interviewer is checking for. **`preventDefault()` is what defers the prompt.** Without it the browser shows its own UI immediately and your saved reference is of little use. **You must keep the event object.** There is no API to ask the browser "can I install now?" and no way to manufacture the prompt later. If you let the event go out of scope, the install button cannot do anything until the browser fires the event again — which may not happen during that page visit. **`prompt()` requires a user gesture and works once.** Calling it on page load, on a timer, or on scroll is refused; calling it twice on the same event object rejects. If the user dismisses the dialog, you need a *new* `beforeinstallprompt` event before you can ask again, which is exactly the anti-nagging design the browser intends. **`userChoice` reports the outcome, not the install.** It resolves to an object whose `outcome` is `'accepted'` or `'dismissed'`. `'accepted'` means the user agreed in the dialog; the `appinstalled` event is the signal that installation actually completed. ## Knowing when to hide the button An install button shown to someone already running the installed app is a bug. Three checks cover it: - The event never fired — the browser has decided the site is not installable, or the app is already installed. Keep the button hidden until the event arrives, and it takes care of itself. - `window.matchMedia('(display-mode: standalone)').matches` is true when the current page is running inside the installed app window. - `navigator.getInstalledRelatedApps()`, where available, reports related installed apps so you can suppress the prompt for users who already have the native app. ## Cross-browser reality `beforeinstallprompt` is a Chromium extension, not a standard implemented everywhere. As of 2025 Safari and Firefox do not fire it. On iOS, installation is a manual "Add to Home Screen" action from the share sheet, which no script can trigger — the honest pattern is to feature-detect: if the event has not fired and the platform is iOS, show a short instructional hint instead of a button that cannot work. Firefox on desktop does not install web apps at all. Because of that unevenness, a site's install rate is not a portable metric, and any product requirement phrased as "prompt every user to install" is unimplementable. What you can build is a button that appears exactly where the platform supports it and stays out of the way otherwise. ## Why the event might never fire When the button never appears in testing, the cause is almost always upstream of this API: the page is not on HTTPS (localhost excepted), the manifest failed to parse or resolve, required icon sizes are missing, `display` is `browser`, or the app is already installed for that profile. Devtools' application panel lists the unmet criteria directly, which is a faster answer than adding logging to the handler.
- A colleague calls `prompt()` from a `setTimeout` two seconds after the event arrives, so every visitor sees the dialog. Why does that not work?`prompt()` is gated on transient user activation. A timer callback carries none, so the call is refused and the promise from `userChoice` never delivers an accepted outcome. The gating is deliberate: it prevents sites from ambushing users with a system-level install dialog they did not ask for.
- The user dismissed the dialog. Can you show it again later in the same page session?Not with the same event object — it is spent after one `prompt()`. You would need the browser to fire `beforeinstallprompt` again, which is its decision, not yours. In practice you record the dismissal, hide the button for a sensible period, and re-show it if a fresh event arrives on a later visit.
- How would you tell, from inside the page, that the user is already running the installed app?Check `window.matchMedia('(display-mode: standalone)').matches`, which is true when the document is rendered in the installed app window rather than a tab. Where relevant, `navigator.getInstalledRelatedApps()` also reports related installed applications. Both are worth wiring to analytics so you can separate app launches from web visits.
saying these in an interview costs you the question
- Believing prompt() can be called on page load without a gesture
- Calling preventDefault() but discarding the event object
- Treating userChoice: 'accepted' as proof the app installed
- Assuming Safari and Firefox fire beforeinstallprompt
- Re-prompting in a loop until the user accepts