Open the address bar on a reasonably well-built progressive web app and Chrome, Edge, or Samsung Internet offers to install it — sometimes a small icon, sometimes a banner, and occasionally nothing at all even though you were certain the site qualified. That gap between “should be installable” and “actually shows the prompt” is almost always a manifest problem, not a browser mood swing. Each engine runs a checklist against your manifest.json and your service worker, and it fails that checklist silently, with no console warning pointing at the cause.
The manifest itself is small — a JSON file with a dozen possible fields — but a handful of them are load-bearing. Get the icon sizes wrong, omit start_url, or leave display on its browser default, and the install affordance just doesn't appear. Nothing crashes. Nothing logs an error. The page keeps working fine as a tab; it simply never becomes an app.
When a product outgrows what the manifest and a service worker can offer — background geofencing, Bluetooth peripherals, push notifications that behave the same way on iOS as on Android — the gap is usually closed by going native, which is where mobile app development services pick up where a PWA's manifest leaves off. Most products never need that jump, though, if the manifest and the install criteria are actually correct, so that's the part worth getting right first.
TL;DR: A PWA installs when its manifest has a name, 192px and 512px icons (one marked maskable), a start_url, a non-browser display mode, is served over HTTPS, and ships a service worker with a fetch handler. Chrome and Edge prompt automatically once those criteria are met; Safari and Firefox require a manual “Add to Home Screen,” but now pass the same Lighthouse PWA audit. Most install failures trace back to a mismatched icon purpose or an inconsistent manifest URL.
A web app manifest is a JSON file — conventionally manifest.json or manifest.webmanifest — linked from the page with <link rel="manifest" href="/manifest.json">. It doesn't run any code. It's pure metadata: the name the OS should show, which icons represent the app at which sizes, what color to paint the splash screen and status bar, which orientation the app prefers, and — critically for this article — how much browser chrome should remain visible once the app is launched from a home screen or app drawer.
That split matters. The manifest describes how the app should look once installed; the service worker is what makes it work offline and qualifies it for installation in the first place. Browsers treat the two as separate, mandatory ingredients of the same recipe — a flawless manifest with no service worker won't install, and a bulletproof service worker with a broken manifest won't either.
Chromium's installability checklist only cares about a handful of manifest members, and most failures come from getting one of these subtly wrong rather than omitting it outright:
name and short_name — the full and abbreviated app names. At least one is required.icons — at least one icon at 192×192 and one at 512×512, declared with accurate sizes and type values.start_url — the page the app opens on launch, resolved relative to the manifest, not the current page.display — must be standalone, fullscreen, or minimal-ui. The implicit default, browser, disqualifies the app.theme_color — optional for installability, but it's what paints the status bar and task-switcher chrome, so skipping it looks unfinished even when the install prompt works fine.A minimal manifest that satisfies every one of those is short enough to hand-write:
{
"name": "Field Notes",
"short_name": "FieldNotes",
"start_url": "/",
"display": "standalone",
"background_color": "#0d111b",
"theme_color": "#0098f2",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}
For the full, authoritative field list — including the newer id, scope, and shortcuts members — MDN's manifest reference is the source worth bookmarking; it's updated as browsers add fields faster than most blog posts can track.
The 192px and 512px sizes aren't arbitrary. Android uses the smaller one for the app drawer and task switcher and the larger one for the splash screen Chrome generates while the app boots. Supply only one size and the browser will scale it — usually badly, with visible compression artifacts on the splash screen.
The trap is purpose: "maskable". Android applies adaptive icon masks — circle, squircle, rounded square, depending on the device's launcher — and clips whatever falls outside a safe zone roughly 80% of the icon's width. An icon designed for a square frame, marked maskable without redrawing it to sit inside that safe zone, gets its logo or text clipped at the edges on real devices, even though it previews perfectly in a design tool. The fix is to either supply a dedicated maskable icon with generous padding, or omit purpose: "maskable" entirely and let the browser apply its own background-and-pad fallback — which looks plainer but never clips content.
The four display values aren't a quality ladder — each trades browser affordances for immersion, and the right one depends on what the app needs the user to be able to do without leaving it:
| display value | What the user sees | Use it when |
|---|---|---|
| standalone | No browser UI, but the OS status bar and back gesture remain | The default choice for almost any installable app |
| fullscreen | The status bar is hidden too — the app owns the entire screen | Games and immersive media; rarely anything else |
| minimal-ui | A trimmed browser chrome with back and reload controls visible | Content apps that still want a visible reload affordance |
| browser | A normal browser tab — no install prompt is offered at all | Never, for an app you actually want installed; it's the value that disqualifies you |
Most teams should default to standalone and only reach for fullscreen when the status bar actively competes with the content for the same pixels.
Three requirements sit underneath the manifest fields and get almost no attention because they're binary: either you clear them or the browser won't even evaluate your manifest.
The page must be served over HTTPS, with localhost treated as an exception for local development. A service worker must be registered and successfully controlling the page's scope — a worker file that throws during installation, or one scoped to a subpath that doesn't cover start_url, counts as not having one. And the manifest must be fetched from the same origin as the document that links to it; a manifest served from a CDN subdomain, even with permissive CORS headers, fails the same-origin check that installability relies on.
This is the practical checklist — the one worth running before you file a browser bug, because the cause is almost always local:
href on the <link> tag can resolve correctly on the homepage and 404 on a nested route.display is still browser, the implicit value when the field is omitted entirely rather than explicitly set.sizes attribute doesn't match the actual pixel dimensions of the referenced file — browsers trust the declared value over inspecting the image.start_url — a worker registered at /app/sw.js with no explicit scope argument defaults to /app/, not the site root.id, even if the visible icon or name has since changed — Chrome treats it as the same app and won't re-offer it.Chromium browsers fire a beforeinstallprompt event before showing their own install banner, and calling event.preventDefault() on it lets you defer that banner and trigger it later from a button inside your own UI — a pattern most teams reach for once the generic browser banner feels disconnected from the rest of the product.
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (event) => {
event.preventDefault();
deferredPrompt = event;
showOwnInstallButton();
});
installButton.addEventListener('click', async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
deferredPrompt = null;
hideOwnInstallButton();
});
The event can only be replayed once — call .prompt() a second time on the same captured event and it's a no-op. The other common mistake is firing the custom button immediately on page load: Chrome's own engagement heuristics (repeat visits, time on site) exist precisely to stop sites from nagging first-time visitors, and routing around them with your own UI the moment the page loads defeats the point and annoys users who haven't decided anything yet. Safari doesn't fire beforeinstallprompt at all — there's no deferred-prompt pattern to build there, only the manual “Add to Home Screen” flow.
Chrome and Edge remain the most complete implementation: automatic installability evaluation, the beforeinstallprompt event, and desktop installation alongside mobile. Firefox supports installable PWAs on desktop, with more limited exposure on Android, and doesn't fire beforeinstallprompt either — it surfaces its own install affordance in the address bar when the manifest qualifies.
Safari still has no automatic install banner and no beforeinstallprompt equivalent; users add a site to the home screen manually through the share sheet. What's changed this year is that Safari now evaluates manifest validity closely enough to pass a Lighthouse PWA installability audit when the same criteria used for Chrome are met — a meaningful shift from the years when Safari's manifest support was too partial to score cleanly on the same checklist at all. It still won't prompt automatically, but a correctly built manifest is no longer a Chrome-only investment.
Chrome DevTools' Application panel has a dedicated Manifest tab that parses your file and flags missing or malformed fields directly, including icon and installability warnings that previously only surfaced in the separate Issues tab — we covered that diagnostic surface in more detail in an earlier piece on Chrome's manifest diagnostics. Running a Lighthouse PWA audit (built into DevTools, or via the CLI) is the fastest second opinion: it re-runs the same installability checklist the browser itself uses and lists every failing criterion by name rather than making you guess from a missing icon.
Test on a real device where possible. Simulators get the manifest parsing right but can mask icon-masking issues and splash-screen scaling that only show up on an actual Android launcher.
Yes — Chromium-based browsers require a registered service worker with at least a no-op fetch event handler as part of the installability checklist; a manifest alone, without a controlling service worker, won't trigger the prompt.
Not directly. You can capture beforeinstallprompt and replay it from your own UI, but you can't make the event fire before the browser's engagement heuristics — repeat visits, time on site — decide the user is a plausible installer.
Usually a relative <link rel="manifest"> path that resolves differently depending on the current URL's depth. Use a root-relative path like /manifest.json, never a bare manifest.json.
Not as a direct ranking factor, but an installed PWA with offline support and re-engagement tends to improve the return-visit and session-depth signals that correlate with better organic performance over time.
name and short_name?name is the full app name shown on the install dialog and in app listings; short_name is a truncated version — keep it under about 12 characters — used under the home-screen icon where space is tight.
Yes. Browsers refuse to evaluate installability if the manifest URL isn't same-origin with the document linking to it, even when CORS headers are configured correctly.
No. Installation is per-browser and per-OS. A user who installs your PWA through Chrome on Android gets no equivalent install on an iPhone unless they separately add it to the home screen in Safari.
None of this is complicated once it's written down — it's just unforgiving about small mismatches between what the manifest claims and what the files on disk actually contain. Get the fields right, keep the manifest and service worker scope aligned, and the install prompt stops being a mystery and starts being a checklist you can tick off before every release.