Skip to content

Install Prompt — Triggering PWA Installation to Home Screen

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Install Prompt. We cover key concepts, practical examples, and best practices to help you master this topic.

The install prompt (beforeinstallprompt event) lets users add your PWA to their home screen, increasing engagement and creating a native app-like presence on the device.

What You'll Learn

By the end of this tutorial, you will understand how the beforeinstallprompt event works, how to show a custom install button, how to defer the prompt, and how to handle the user's install decision.

Why It Matters

Installation is what transforms a website into a PWA in the user's mind. Installed PWAs appear on the home screen, open in standalone mode, and have higher engagement. Implementing a thoughtful install prompt can increase installation rates by 5-10x compared to the default browser prompt.

Real-World Use

A music streaming PWA shows a custom install banner after the user listens to 5 songs. The banner says "Add to your home screen for quick access and offline playback." The custom prompt converts 40% of users compared to 8% for the default browser prompt.

Install Prompt Flow

Install Prompt Flow
    User meets PWA install criteria
    (service worker, manifest, HTTPS, engagement)
         ↓
    Browser fires 'beforeinstallprompt' event
         ↓
    ╔══════════════════════════════════════╗
    ║  Call event.preventDefault() to      ║
    ║  defer the prompt for our own UI     ║
    ╚══════════════════════════════════════╝
         ↓
    Show a custom install button or banner
         ↓
    User clicks custom install button
         ↓
    Call event.prompt() to show the browser dialog
         ↓
    ╔═══════════════════════════╗
    ║  User installs or cancels ║
    ╚═══════════════════════════╝
         ↓
    ┌──────────┐             ┌──────────────┐
    │ Installed │            │  Canceled    │
    └─────┬─────┘            └──────┬───────┘
          ↓                         ↓
    Hide install button      Can prompt again
    permanently              (up to 3 times)

Think of the install prompt like a store asking if you want to become a member. The browser offers the prompt (like a cashier asking), but you can defer it to your own timing (like asking after the customer has shopped a few times). A well-timed ask gets more yeses than an immediate ask.

Capturing the Install Prompt

let deferredPrompt = null;
let installButton = null;

window.addEventListener('beforeinstallprompt', event => {
    // Prevent the default mini-infobar from appearing
    event.preventDefault();

    // Store the event for later use
    deferredPrompt = event;

    // Show our custom install button
    showInstallButton();

    console.log('Install prompt captured');
});

function showInstallButton() {
    installButton = document.getElementById('install-button');
    if (installButton) {
        installButton.style.display = 'block';
        installButton.addEventListener('click', showInstallPrompt);
    }
}

Showing the Install Prompt

async function showInstallPrompt() {
    if (!deferredPrompt) {
        console.log('No install prompt available');
        return;
    }

    // Show the browser install prompt
    deferredPrompt.prompt();

    // Wait for the user's response
    const choiceResult = await deferredPrompt.userChoice;

    if (choiceResult.outcome === 'accepted') {
        console.log('User accepted the install prompt');
        trackInstallAccepted();
    } else {
        console.log('User dismissed the install prompt');
        trackInstallDismissed();
    }

    // Reset the deferred prompt
    deferredPrompt = null;
    hideInstallButton();
}

function hideInstallButton() {
    if (installButton) {
        installButton.style.display = 'none';
    }
}

Custom Install UI

function showInstallBanner() {
    const banner = document.getElementById('install-banner');
    if (!banner) return;

    // Check if app is already installed
    if (window.matchMedia('(display-mode: standalone)').matches) {
        banner.style.display = 'none';
        return;
    }

    banner.style.display = 'block';
    banner.querySelector('.install-button')
        .addEventListener('click', showInstallPrompt);

    banner.querySelector('.dismiss-button')
        .addEventListener('click', () => {
            banner.style.display = 'none';
            // Show again after 7 days
            localStorage.setItem('install-banner-dismissed', Date.now());
        });
}

// Check if we should show the banner
function shouldShowInstallBanner() {
    const dismissed = localStorage.getItem('install-banner-dismissed');
    if (dismissed) {
        const daysSinceDismiss = (Date.now() - parseInt(dismissed)) / (1000 * 60 * 60 * 24);
        if (daysSinceDismiss < 7) return false;
    }
    return true;
}

Tracking Install State

// Check if app is running in standalone mode
function isRunningStandalone() {
    return window.matchMedia('(display-mode: standalone)').matches ||
           window.navigator.standalone ||
           document.referrer.includes('android-app://');
}

console.log('Running standalone:', isRunningStandalone());

// Listen for the appinstalled event
window.addEventListener('appinstalled', event => {
    console.log('PWA was installed');
    trackInstallCompleted();
    hideInstallButton();
});

// Track display mode changes
window.matchMedia('(display-mode: standalone)').addEventListener('change', event => {
    if (event.matches) {
        console.log('App moved to standalone mode');
        sendAnalytics('pwa_installed');
    }
});

Install Prompt Best Practices

// Defer install prompt until user has shown engagement
let pageVisits = parseInt(localStorage.getItem('page-visits') || '0');

function trackPageVisit() {
    pageVisits++;
    localStorage.setItem('page-visits', pageVisits.toString());
}

function shouldPromptInstall() {
    const conditions = {
        supported: !!deferredPrompt,
        notInstalled: !isRunningStandalone(),
        sufficientVisits: pageVisits >= 3,
        recentDismiss: checkDismissCooldown(),
        engaged: checkUserEngagement()
    };

    console.log('Install conditions:', conditions);

    return conditions.supported &&
           conditions.notInstalled &&
           conditions.sufficientVisits &&
           !conditions.recentDismiss &&
           conditions.engaged;
}

function checkUserEngagement() {
    const timeOnSite = parseInt(sessionStorage.getItem('time-on-site') || '0');
    return timeOnSite > 60000; // More than 1 minute
}

function checkDismissCooldown() {
    const lastDismiss = localStorage.getItem('install-dismissed');
    if (!lastDismiss) return false;
    return (Date.now() - parseInt(lastDismiss)) < 7 * 24 * 60 * 60 * 1000;
}

// Conditionally show after user engagement
window.addEventListener('beforeinstallprompt', event => {
    event.preventDefault();
    deferredPrompt = event;

    if (shouldPromptInstall()) {
        setTimeout(showInstallBanner, 2000);
    }
});

Common Mistakes

  1. Showing the install prompt immediately. Users who just arrived do not install. Wait for sufficient engagement (visits, time, actions).
  2. Not checking if already installed. If the user already installed, do not show the prompt again. Check matchMedia(display-mode: standalone).
  3. Calling prompt() without user gesture. The prompt() must be called in response to a user action (click or tap). Calling it programmatically fails.
  4. Not deferring with preventDefault(). Without preventDefault(), the browser shows its own mini-infobar which is less effective than a custom prompt.
  5. Showing the prompt after it was dismissed too many times. Track dismissals and stop showing after 3 attempts to avoid annoying users.

Practice Questions

  1. What is the beforeinstallprompt event and why should you capture it?
  2. Why must you call event.preventDefault() on the beforeinstallprompt event?
  3. When should you show the custom install button relative to user engagement?
  4. How do you detect if the PWA is already installed?
  5. What happens after the user accepts the install prompt?

Challenge: Implement a complete install prompt flow: capture beforeinstallprompt, defer it, show a custom banner after 3 page visits or 60 seconds of engagement, call prompt() on the deferred event on button click, handle installation acceptance/dismissal, and stop showing after 3 dismissals.

FAQ

Will the install prompt always fire?

No. The browser fires beforeinstallprompt only when the PWA meets certain criteria: HTTPS, registered service worker with fetch handler, valid manifest with icons, and some user engagement.

Can I trigger the install prompt on iOS?

No. Safari does not support the beforeinstallprompt event. On iOS, users must manually add to home screen via the Share menu.

Can I customize the install prompt dialog?

The browser install dialog (shown by prompt()) cannot be customized. However, you can create a fully custom install banner that appears before you call prompt().

How do I know if the user installed the app?

Listen for the 'appinstalled' event on the window. Also check matchMedia('(display-mode: standalone)') after the user returns.

Can I prompt again if the user declines?

Yes, but limit attempts. Chrome allows beforeinstallprompt to fire multiple times. Track dismissals and stop after 3 attempts to avoid annoying the user.

Mini Project

Create a custom install prompt system: capture beforeinstallprompt, show a branded install banner after the user visits 3 different pages or spends 2 minutes on site, include install and dismiss buttons, track install state, hide permanently after install, and stop showing after 3 dismissals. Test by clearing site data and going through the flow.

What's Next

Install prompt is handled. Now learn the App Shell architecture — loading a minimal application shell instantly and populating content dynamically.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro