PWA Install on Device (Add to Home Screen)
Add to Home Screen — mechanism allowing user to install web app on device without app store. Not just shortcut: installed PWA launches without browser bar, gets separate icon, appears in app list and, on Android, in installer. Difference in retention measured: average CTR for installed PWA 50–70% higher than bookmarks.
Requirements for prompt appearance
Browser shows install prompt only when multiple conditions met:
- Site works on HTTPS (localhost exception during development)
- Service Worker registered intercepting minimum fetch-event
-
manifest.jsonpresent with required fields:name,short_name,start_url,display, and 192px/512px icons - User spent sufficient time on site (Chrome uses own heuristic, exact thresholds undocumented)
On iOS no automatic prompt — only manual via "Share → Add to Home Screen". Safari limitation can't be bypassed.
Intercept beforeinstallprompt event
Chrome and Edge generate beforeinstallprompt before showing system dialog. Must intercept and show custom UI:
let deferredPrompt = null;
window.addEventListener('beforeinstallprompt', (e) => {
// Prevent automatic show
e.preventDefault();
deferredPrompt = e;
// Show custom button
showInstallBanner();
});
async function triggerInstall() {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
// Log result
gtag('event', 'pwa_install', {
event_category: 'PWA',
event_label: outcome, // 'accepted' | 'dismissed'
});
deferredPrompt = null;
hideInstallBanner();
}
window.addEventListener('appinstalled', () => {
// App installed — hide banners
hideInstallBanner();
deferredPrompt = null;
});
Important: prompt() can be called only once. After call deferredPrompt becomes invalid. If user declined — next beforeinstallprompt event wait up to weeks (Chrome won't re-offer before ~90 days after decline).
Detect launch context
Need to understand in which mode app running:
function getDisplayMode() {
if (window.matchMedia('(display-mode: standalone)').matches) {
return 'standalone';
}
if (window.matchMedia('(display-mode: fullscreen)').matches) {
return 'fullscreen';
}
if (window.navigator.standalone === true) {
// iOS Safari
return 'standalone-ios';
}
return 'browser';
}
// Check on load
const mode = getDisplayMode();
if (mode === 'browser') {
// Can show install hint
}
// Listen for changes
window.matchMedia('(display-mode: standalone)').addEventListener('change', (e) => {
if (e.matches) {
console.log('App is now in standalone mode');
}
});
iOS banner
Safari on iOS doesn't support beforeinstallprompt. Must show custom instruction:
function shouldShowIOSPrompt() {
const isIOS = /iphone|ipad|ipod/i.test(navigator.userAgent);
const isInStandaloneMode = window.navigator.standalone === true;
const hasSeenPrompt = localStorage.getItem('ios-install-prompt-shown');
return isIOS && !isInStandaloneMode && !hasSeenPrompt;
}
if (shouldShowIOSPrompt()) {
showIOSInstructionBanner();
localStorage.setItem('ios-install-prompt-shown', 'true');
}
Banner must show Share button icon (varies between iOS versions) and text "Tap Share → Add to Home Screen". Use SVG icon matching real system icon.
Manifest
Minimal working manifest.json:
{
"name": "App Name",
"short_name": "App",
"description": "App description for store",
"start_url": "/?source=pwa",
"scope": "/",
"display": "standalone",
"orientation": "any",
"background_color": "#ffffff",
"theme_color": "#1a73e8",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"screenshots": [
{
"src": "/screenshots/desktop.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
},
{
"src": "/screenshots/mobile.png",
"sizes": "390x844",
"type": "image/png",
"form_factor": "narrow"
}
]
}
purpose: "maskable" important for Android — system can clip icon to shape. Icon must have safe zone: content not closer than 10% from edges.
screenshots appear in install dialog in Chrome 108+ — makes prompt more convincing.
Install funnel analytics
Without analytics unclear where users drop:
const INSTALL_FUNNEL = {
BANNER_SHOWN: 'install_banner_shown',
BANNER_CLICKED: 'install_banner_clicked',
PROMPT_SHOWN: 'install_prompt_shown',
PROMPT_ACCEPTED: 'install_prompt_accepted',
PROMPT_DISMISSED: 'install_prompt_dismissed',
APP_INSTALLED: 'app_installed',
};
function trackInstallStep(step, params = {}) {
gtag('event', step, {
event_category: 'pwa_install_funnel',
...params,
});
}
Standard funnel: banner show → click → system prompt → accept. Typical prompt conversion — 20–40% with motivated audience.
Timeline
- Manifest + basic Service Worker + event intercept: 1 day
- Custom install banner with analytics: 1 day
- iOS support (instruction + mode detect): 0.5 day
- Maskable icons + design adaptation: 0.5 day
Total: 2–3 days from zero to full implementation with analytics.







