Introduction
Progressive Web Apps (PWAs) bridge the gap between websites and native apps. With a Web App Manifest and Service Worker, your website can be installed on a user’s home screen, work offline, and feel like a native app — without going through an app store.
What Makes a PWA “Installable”?
For a browser to offer the “Add to Home Screen” prompt, your app needs:
- HTTPS — required for security
- Web App Manifest — describes the app (name, icons, colors)
- Service Worker — enables offline functionality
- Engagement criteria — user has visited the site multiple times (Chrome’s heuristic)
The Web App Manifest
The manifest is a JSON file that tells the browser how to display your app when installed. Place it at the root of your site (e.g., /manifest.json) and link it in your HTML.
manifest.json
{
"name": "My Awesome App",
"short_name": "MyApp",
"description": "A progressive web app that does amazing things",
"start_url": "/",
"display": "standalone",
"orientation": "portrait",
"background_color": "#ffffff",
"theme_color": "#2196F3",
"lang": "en",
"scope": "/",
"icons": [
{
"src": "/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
}
],
"screenshots": [
{
"src": "/screenshots/desktop.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide",
"label": "Desktop view"
},
{
"src": "/screenshots/mobile.png",
"sizes": "390x844",
"type": "image/png",
"form_factor": "narrow",
"label": "Mobile view"
}
]
}
Link the Manifest in HTML
<head>
<link rel="manifest" href="/manifest.json">
<!-- Theme color for browser chrome -->
<meta name="theme-color" content="#2196F3">
<!-- iOS Safari specific (doesn't use manifest) -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="MyApp">
<link rel="apple-touch-icon" href="/icons/icon-192x192.png">
<!-- Windows tiles -->
<meta name="msapplication-TileImage" content="/icons/icon-144x144.png">
<meta name="msapplication-TileColor" content="#2196F3">
</head>
Display Modes
The display field controls how the app looks when launched from the home screen:
| Mode | Description | Use Case |
|---|---|---|
standalone |
Looks like a native app — no browser UI | Most apps |
fullscreen |
No browser UI, no status bar | Games |
minimal-ui |
Minimal browser controls (back, reload) | Content sites |
browser |
Regular browser tab | Default web behavior |
Generating Icons
You need icons in multiple sizes. Tools to generate them:
- Favicon Generator — upload one image, get all sizes
- PWA Asset Generator — CLI tool for generating all required assets
- Maskable.app — test and create maskable icons
# Using pwa-asset-generator
npx pwa-asset-generator logo.png ./icons \
--manifest manifest.json \
--index index.html
Maskable Icons
Maskable icons fill the entire icon shape on Android (no white padding). Add "purpose": "maskable" to icons that are designed for this:
{
"src": "/icons/icon-192x192-maskable.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
}
Registering a Service Worker
A Service Worker is required for full PWA functionality (offline support, background sync, push notifications):
// In your main JavaScript file
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('Service Worker registered:', registration.scope);
} catch (error) {
console.error('Service Worker registration failed:', error);
}
});
}
// sw.js — basic caching service worker
const CACHE_NAME = 'myapp-v1';
const ASSETS_TO_CACHE = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/icons/icon-192x192.png'
];
// Install: cache assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS_TO_CACHE))
);
});
// Fetch: serve from cache, fall back to network
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request);
})
);
});
// Activate: clean up old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))
)
)
);
});
Handling the Install Prompt
You can intercept the browser’s install prompt and show it at the right moment:
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (event) => {
// Prevent the default mini-infobar
event.preventDefault();
deferredPrompt = event;
// Show your custom install button
document.getElementById('install-btn').style.display = 'block';
});
document.getElementById('install-btn').addEventListener('click', async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log(`User ${outcome === 'accepted' ? 'accepted' : 'dismissed'} the install prompt`);
deferredPrompt = null;
document.getElementById('install-btn').style.display = 'none';
});
// Track successful installation
window.addEventListener('appinstalled', () => {
console.log('PWA was installed');
deferredPrompt = null;
});
iOS Safari Considerations
iOS Safari doesn’t support the beforeinstallprompt event or automatic install prompts. Users must manually add to home screen via the Share menu. You need to:
- Provide
apple-touch-iconmeta tags - Show a manual instruction to iOS users
// Detect iOS
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
const isInStandaloneMode = window.matchMedia('(display-mode: standalone)').matches;
if (isIOS && !isInStandaloneMode) {
// Show iOS-specific install instructions
document.getElementById('ios-install-hint').style.display = 'block';
}
<!-- iOS install hint -->
<div id="ios-install-hint" style="display:none">
<p>Install this app: tap <img src="/icons/share.svg" alt="Share"> then "Add to Home Screen"</p>
</div>
Testing Your PWA
Chrome DevTools
- Open DevTools → Application tab
- Check “Manifest” — verify all fields are correct
- Check “Service Workers” — verify registration
- Run “Lighthouse” audit → PWA category
Lighthouse CLI
npm install -g lighthouse
lighthouse https://yoursite.com --view --preset=desktop
PWA Checklist
- HTTPS enabled
- manifest.json linked in HTML
- Icons in 192x192 and 512x512 (minimum)
-
start_urlset correctly -
display: standaloneorfullscreen - Service Worker registered
- Offline page works
-
theme_colorandbackground_colorset - iOS meta tags added
- Lighthouse PWA score ≥ 90
Resources
- MDN: Web App Manifests
- web.dev: Progressive Web Apps
- PWA Builder — generate manifest and service worker
- Workbox — service worker library by Google
- Maskable.app — test maskable icons
Comments