Customize PWA Install Flow with beforeinstallprompt
The beforeinstallprompt event gives web development
teams full control over how, when, and where a Progressive Web App (PWA)
prompts users for installation. By intercepting this browser-level
event, developers can suppress default, generic installation banners and
replace them with custom, contextual user interfaces. This article
explains how the event functions, how to capture and defer it, and how
teams leverage it to build higher-converting, user-friendly installation
journeys.
Intercepting the Default Browser Prompt
By default, Chromium-based browsers trigger an automatic mini-infobar or ambient badge when a PWA meets the core installability criteria (such as having a valid web app manifest, a registered service worker, and serving over HTTPS). While functional, this default prompt often appears prematurely—frequently on a user's first visit before they have experienced the app's core value.
The beforeinstallprompt event fires right before the
browser shows this default prompt. Developers can listen for this event
and call e.preventDefault(), which prevents the browser
from displaying its standard installation UI.
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
// Prevent the default mini-infobar from appearing
e.preventDefault();
// Stash the event so it can be triggered later
deferredPrompt = e;
// Update UI notify the user they can install the PWA
showInstallPromotion();
});Storing and Deferring the Event
Calling preventDefault() does not cancel the
installation capability; it simply puts the process on hold. By saving
the event object to a global or scoped variable (often named
deferredPrompt), development teams can retain the ability
to launch the native installation dialog at an optimal moment later in
the user's journey.
Creating Contextual and Branded UI
Once the event is captured, teams can reveal custom UI components that match the application's design system. Common implementation patterns include:
- Navigation Bar Buttons: A persistent "Install App" button in the header or sidebar that only becomes visible once the event fires.
- Contextual Banners: An inline banner explaining the specific benefits of installing, such as offline access, faster load times, or push notifications.
- Milestone Triggers: Prompting the user after they complete a meaningful action, such as bookmarking an item, finishing a checkout, or completing a level in a game.
Triggering the Native Install Dialog
When the user interacts with the custom installation UI, the stored
event can be invoked using its .prompt() method. This
re-engages the browser's native flow, presenting the official
confirmation dialog to the user.
installButton.addEventListener('click', async () => {
if (!deferredPrompt) {
return;
}
// Show the browser install prompt
deferredPrompt.prompt();
// Wait for the user to respond to the prompt
const { outcome } = await deferredPrompt.userChoice;
// Log the outcome for analytics
console.log(`User response to install prompt: ${outcome}`);
// Clear the deferredPrompt variable since it can only be used once
deferredPrompt = null;
hideInstallPromotion();
});Analyzing User Response
with userChoice
The beforeinstallprompt event provides a
userChoice property, which returns a promise that resolves
with an object containing the user's decision (accepted or
dismissed).
This mechanism allows development teams to:
- Track conversion metrics and identify which UI triggers generate the highest install rates.
- Gracefully update the UI if the user declines (for instance, hiding the custom button or waiting a set number of days before showing another promotion).
- Trigger onboarding flows or confirmation messages when
outcome === 'accepted'.
Handling Post-Installation Clean-Up
In addition to userChoice, teams typically pair
beforeinstallprompt with the appinstalled
event. Once an installation completes, the browser fires
appinstalled, signaling that the application should remove
any remaining install banners and adjust the interface to reflect that
the app is now installed locally on the user's device.