Have you ever gone onto a website and you get that little banner at the top asking you if you would like to install the app? Well this is what we are going to be looking at implementing today.
It actually isn't too difficult, but for a browser to prompt a user to install your PWA, you need to have a couple of things in place to make it eligible. You need to serve your page over HTTPS, make up a manifest.json file at the root of your URL to give devices details about your app, and you need to have a registered service worker installed on the client's browser. A service worker is a script that runs in the background of your website, they are useful for caching, notifications, making custom offline pages, and a few other things.
Let's start by setting up our manifest.json file:
{
"id": "/",
"name": "KOWD Development",
"short_name": "KOWD",
"start_url": "/",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#48cc44",
"icons": [
{
"src": "/images/logo-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/images/logo-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/images/logo_curved_corners-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/images/logo_curved_corners-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/images/logo_circle-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/images/logo_circle-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
So let's see whats going on here with code comments below. JSON however does not support code comments, so I will put a code box below without the copy option:
{
"id": "/", // Shows the unique identifier for a specific version of the app to avoid ambiguity when you browse other pages on the website. Usually mirrors the start_url
"name": "Kieran Oldfield Web Development", // Long name for app
"short_name": "KOWD", // Short name for app - usually appears when downloaded on mobile device
"start_url": "/", // The URL directory you would like to open once the app is opened
"display": "standalone", // Specifying you want it to be a standalone app
"background_color": "#000000", // Colour you would like the theme of the app background to be - important when opening the app to match your theme
"theme_color": "#48cc44", // Colour that blends with stuff like your widgets on a mobile device - ideal to have this the same as your navigation bar
"icons": [
// Main two icons that will appear when your app is downloaded
{
"src": "/images/logo-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/images/logo-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
// Other shaped icons that support other devices layouts
{
"src": "/images/logo_curved_corners-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/images/logo_curved_corners-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/images/logo_circle-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/images/logo_circle-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
Perfect! Now just set this JSON file to be served at the root of your directory, and let's also put a link tag in our base HTML file's head tags:
<link rel="manifest" href="/manifest.json">
Great! Next, we need to set up a simple service worker. You can add more features and functions to this service worker as you wish, but for simplicity's sake I'm just going to set up something simple that caches your home/root URL on activation of the service worker:
// The name you want to call the cache in your browser
const cacheName = "cache-v1";
// When your service worker installs, open the cache and add your root URL
self.addEventListener("install", async (event) => {
try {
const cache = await caches.open(cacheName);
await cache.add('/');
} catch (error) {
console.error("Service Worker installation failed:", error);
}
});
// When the service worker activates, delete all old caches apart from the one you've just opened on install
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cn => {
if (cn !== cacheName) {
return caches.delete(cn);
}
})
);
})
);
});
With your service worker script created (e.g., call it service-worker.js), we now need to make sure it actually gets registered by the browser. This part goes into your main JavaScript file, or directly into a script tag in your HTML depending on how your site is structured. Here's the snippet to register the service worker:
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker.register("/service-worker.js")
.then(registration => {
console.log("Service Worker registered with scope:", registration.scope);
})
.catch(error => {
console.error("Service Worker registration failed:", error);
});
});
}
Now that your manifest.json is in place and your service worker is registered, the final optional step is to handle the "beforeinstallprompt" event. This gives you control over when and how the browser shows the install banner to users. This is optional, because you have now already filled the criteria to make your website a PWA, so the install app banner will show automatically at your browsers discretion and your website is now installable as an app when you click "Add to Home" on your browser's options.
Here’s a simple example of how to work with that:
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault(); // Prevent the mini-infobar from appearing
deferredPrompt = e;
const installBtn = document.getElementById("install-btn");
if (installBtn) {
installBtn.style.display = "block";
installBtn.addEventListener("click", () => {
installBtn.style.display = "none";
deferredPrompt.prompt();
deferredPrompt.userChoice.then((choiceResult) => {
if (choiceResult.outcome === 'accepted') {
console.log('User accepted the A2HS prompt');
} else {
console.log('User dismissed the A2HS prompt');
}
deferredPrompt = null;
});
});
}
});
All that’s left now is to create a simple install button somewhere in your HTML:
<button id="install-btn" style="display: none;">Install App</button>
And that’s it! You now have a basic working setup for a Progressive Web App with a custom install prompt. There’s loads more you can do with service workers and the manifest file, like offline pages, advanced caching strategies, and push notifications - but this gives you a great starting point.
Thanks for reading! Let me know if you got stuck at any part or want me to go deeper into any specific feature of PWAs in a future post.
Happy coding!