Progressive Web Apps (PWA)

App Installation (Add to Home Screen)

19 min Lesson 10 of 30

App Installation and Add to Home Screen

One of the defining features of PWAs is the ability to be installed on a user's device, providing an app-like experience that launches from the home screen. This lesson covers how to implement and customize the installation experience.

Installation Criteria

Before a PWA can be installed, it must meet certain requirements:

Installation Requirements:
  • HTTPS: Served over a secure connection (except localhost)
  • Web App Manifest: Valid manifest.json file with required fields
  • Service Worker: Registered service worker that handles fetch events
  • Icons: At least one icon (192x192 or larger)
  • User Engagement: User has interacted with the site (Chrome only)

The beforeinstallprompt Event

Capture the install prompt and display it at an appropriate time:

let deferredPrompt; let installButton; window.addEventListener('beforeinstallprompt', (event) => { // Prevent the default install prompt event.preventDefault(); // Store the event for later use deferredPrompt = event; // Show custom install button installButton = document.getElementById('install-button'); if (installButton) { installButton.style.display = 'block'; } console.log('Install prompt available'); }); // Handle install button click if (installButton) { installButton.addEventListener('click', async () => { if (!deferredPrompt) { console.log('Install prompt not available'); return; } // Show the install prompt deferredPrompt.prompt(); // Wait for user response const { outcome } = await deferredPrompt.userChoice; console.log(`User response: ${outcome}`); if (outcome === 'accepted') { console.log('User accepted the install prompt'); } else { console.log('User dismissed the install prompt'); } // Clear the deferred prompt deferredPrompt = null; // Hide install button installButton.style.display = 'none'; }); }
Best Practices for Install Prompts:
  • Wait for user engagement before showing the prompt
  • Provide context about why installing is beneficial
  • Make the install button visually appealing but not intrusive
  • Don't show the prompt immediately on page load
  • Respect users who dismiss the prompt

Custom Install UI

Create a custom install prompt that fits your app's design:

<!-- Custom install banner --> <div id="install-banner" class="install-banner" style="display: none;"> <div class="banner-content"> <img src="/icon-192x192.png" alt="App Icon" class="banner-icon"> <div class="banner-text"> <h3>Install Our App</h3> <p>Get quick access and work offline</p> </div> <div class="banner-actions"> <button id="install-accept" class="btn-primary">Install</button> <button id="install-dismiss" class="btn-secondary">Not Now</button> </div> </div> </div> <style> .install-banner { position: fixed; bottom: 0; left: 0; right: 0; background: white; box-shadow: 0 -2px 10px rgba(0,0,0,0.1); padding: 16px; z-index: 1000; } .banner-content { display: flex; align-items: center; gap: 16px; max-width: 1200px; margin: 0 auto; } .banner-icon { width: 64px; height: 64px; border-radius: 12px; } .banner-text { flex: 1; } .banner-text h3 { margin: 0 0 4px 0; font-size: 18px; } .banner-text p { margin: 0; color: #666; font-size: 14px; } .banner-actions { display: flex; gap: 8px; } </style> <script> let deferredPrompt; const installBanner = document.getElementById('install-banner'); const installAccept = document.getElementById('install-accept'); const installDismiss = document.getElementById('install-dismiss'); window.addEventListener('beforeinstallprompt', (event) => { event.preventDefault(); deferredPrompt = event; // Show banner after 30 seconds of engagement setTimeout(() => { if (installBanner) { installBanner.style.display = 'block'; } }, 30000); }); installAccept?.addEventListener('click', async () => { if (deferredPrompt) { deferredPrompt.prompt(); const { outcome } = await deferredPrompt.userChoice; console.log(`Install outcome: ${outcome}`); deferredPrompt = null; } installBanner.style.display = 'none'; }); installDismiss?.addEventListener('click', () => { installBanner.style.display = 'none'; // Remember dismissal for 7 days localStorage.setItem('install-dismissed', Date.now() + (7 * 24 * 60 * 60 * 1000)); }); </script>

Tracking Installation

Monitor when users install your PWA and how they launch it:

// Track when app is installed window.addEventListener('appinstalled', (event) => { console.log('PWA was installed'); // Send analytics event if (window.gtag) { gtag('event', 'pwa_install', { method: 'browser_prompt' }); } // Hide any install prompts const installButton = document.getElementById('install-button'); if (installButton) { installButton.style.display = 'none'; } }); // Detect how app was launched function detectLaunchMode() { const isStandalone = window.matchMedia('(display-mode: standalone)').matches; const isInWebAppiOS = (window.navigator.standalone === true); const isInWebAppChrome = window.matchMedia('(display-mode: standalone)').matches; if (isStandalone || isInWebAppiOS || isInWebAppChrome) { console.log('Launched as installed PWA'); // Track standalone launch if (window.gtag) { gtag('event', 'pwa_launch', { display_mode: 'standalone' }); } } else { console.log('Launched in browser'); } } detectLaunchMode();

App Shortcuts

Define app shortcuts in your manifest to provide quick access to key features:

{ "name": "My PWA", "short_name": "PWA", "start_url": "/", "display": "standalone", "shortcuts": [ { "name": "New Message", "short_name": "Message", "description": "Compose a new message", "url": "/messages/new", "icons": [ { "src": "/images/shortcut-message.png", "sizes": "192x192", "type": "image/png" } ] }, { "name": "Settings", "short_name": "Settings", "description": "Open app settings", "url": "/settings", "icons": [ { "src": "/images/shortcut-settings.png", "sizes": "192x192", "type": "image/png" } ] } ] }
Platform Limitations: App shortcuts are currently supported on Android and Windows, but not on iOS. Always test on target platforms.

App Badges

Update the app icon badge to show notification counts or status:

// Set badge count if ('setAppBadge' in navigator) { navigator.setAppBadge(5); // Shows "5" on app icon } // Clear badge if ('clearAppBadge' in navigator) { navigator.clearAppBadge(); } // Update badge when receiving notifications self.addEventListener('push', async (event) => { const data = event.data.json(); // Show notification await self.registration.showNotification(data.title, { body: data.body, icon: '/images/icon-192x192.png' }); // Update badge count const unreadCount = await getUnreadCount(); if ('setAppBadge' in navigator) { navigator.setAppBadge(unreadCount); } });

Related Applications

Specify related native apps in your manifest:

{ "name": "My PWA", "related_applications": [ { "platform": "play", "url": "https://play.google.com/store/apps/details?id=com.example.myapp", "id": "com.example.myapp" }, { "platform": "itunes", "url": "https://apps.apple.com/app/id123456789" } ], "prefer_related_applications": false }
Note: Set prefer_related_applications to true only if you want browsers to suggest the native app instead of installing the PWA.

Complete Installation Flow

class PWAInstaller { constructor() { this.deferredPrompt = null; this.init(); } init() { window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); this.deferredPrompt = e; this.showInstallPromotion(); }); window.addEventListener('appinstalled', () => { this.handleInstalled(); }); this.detectStandalone(); } showInstallPromotion() { // Check if user dismissed recently const dismissed = localStorage.getItem('install-dismissed'); if (dismissed && Date.now() < parseInt(dismissed)) { return; } // Show install UI const installUI = document.getElementById('install-banner'); if (installUI) { installUI.style.display = 'block'; } } async install() { if (!this.deferredPrompt) return; this.deferredPrompt.prompt(); const { outcome } = await this.deferredPrompt.userChoice; if (outcome === 'accepted') { console.log('User accepted install'); } else { localStorage.setItem('install-dismissed', Date.now() + (7 * 24 * 60 * 60 * 1000)); } this.deferredPrompt = null; this.hideInstallPromotion(); } handleInstalled() { console.log('App installed'); this.hideInstallPromotion(); // Track installation if (window.gtag) { gtag('event', 'pwa_install'); } } hideInstallPromotion() { const installUI = document.getElementById('install-banner'); if (installUI) { installUI.style.display = 'none'; } } detectStandalone() { const isStandalone = window.matchMedia('(display-mode: standalone)').matches; if (isStandalone) { console.log('Running as standalone app'); document.body.classList.add('standalone-mode'); } } } // Initialize const installer = new PWAInstaller();
Exercise:
  1. Implement the beforeinstallprompt event handler
  2. Create a custom install button with promotional text
  3. Track installation events with analytics
  4. Add app shortcuts to your manifest for key features
  5. Implement badge notifications for unread counts
  6. Test the installation flow on different devices