What Is a Progressive Web App?¶
A Progressive Web App (PWA) is a website built with standard web technologies that uses a secure context, a Web App Manifest and (usually) a service worker so that browsers can install it, launch it in its own window, keep it working on unreliable networks and connect it to operating-system features such as notifications, badges, sharing and file handling. It is not a framework, a file format or a store: it is the same URL-addressable site, progressively enhanced with app capabilities wherever the browser supports them. This page gives the precise definition, where the term came from, what each of the original characteristics means in technical terms, how an installed PWA actually runs on each platform, a complete minimal PWA, what PWAs can and cannot do in 2026, and the misconceptions that cause most bad decisions about them.
Key takeaways
- Definition: a PWA is a web app served from a secure context that provides a Web App Manifest (so it can be installed) and typically a service worker (so it can work offline, load instantly and receive push messages). Everything else is a quality attribute, not a requirement.
- Origin: the term was coined by Frances Berriman and Alex Russell and published in Russell's June 15, 2015 essay Progressive Web Apps: Escaping Tabs Without Losing Our Soul.
- Installation requirements have shrunk: Chrome dropped the service-worker requirement for menu installation (Chrome 108 on Android, 112 on desktop), and Safari 26 lets users open any site added to the Home Screen as a web app.
- An installed PWA is still the browser. On desktop Chromium and Android it shares the browser profile's storage; on iOS, iPadOS and macOS Safari each web app gets its own isolated storage.
- Progressive enhancement is the core principle: detect every capability individually, never the "PWA-ness" of the browser.
- Hard limits remain: no general-purpose background execution, uneven capability support (many device APIs are Chromium-only), no programmatic install prompt in Safari or Firefox, and platform-controlled distribution rules.
A precise definition¶
There are three useful ways to define a PWA, and they answer different questions.
The technical definition (what the browser sees): a PWA is a set of same-origin documents that
- are served from a secure context (HTTPS, or
http://localhost/127.0.0.1during development); - link a Web App Manifest with at least a name and icons, which lets the browser offer installation and create an OS-level app entry; and
- usually register a service worker whose scope covers the app, which lets the app control its own network requests, run offline and receive push messages and other background events.
The behavioral definition (what users experience): an app that is capable (uses device and OS features), reliable (loads fast and works regardless of network quality) and installable (lives on the home screen, dock or Start menu and runs in its own window).
The philosophical definition (what makes it progressive): the app works as a normal website for everyone, in every browser, and adds app-like capabilities only where they are supported and only as the user opts in. There is no second codebase and no separate "app version".
A handful of terms recur throughout this site and are worth pinning down now:
- Web app
- Any web site whose primary purpose is interactive functionality rather than documents. Not every web app is a PWA, and not every PWA is a single-page app.
- Installed web app
- A web app with an OS-level entry (launcher icon, Start menu item, Dock icon) that opens in its own window. Browsers use different words for the same thing: Chrome and Edge say "app" or "install", Apple says "web app" or "Home Screen web app", Firefox on Windows calls them "web apps" pinned to the taskbar.
- Manifest
- The JSON file linked with
<link rel="manifest" href="...">and served asapplication/manifest+json. It is defined by the W3C Web Application Manifest specification, which is still a Working Draft maintained by the Web Applications Working Group even though large parts of it have shipped everywhere. See Web App Manifest. - Service worker
- An event-driven worker script, registered for a URL scope, that the browser starts on demand to handle
fetch,push,notificationclick,syncand other events, and stops when idle. See Service Workers.
PWA is not a formal standard
No specification defines "Progressive Web App". The term is an umbrella over several independent standards: Service Workers, the Web Application Manifest, the Push API, the Notifications API, Cache Storage, IndexedDB and many capability APIs. Browser vendors each decide which combination unlocks installation and other app features. That is why the requirements differ per browser and why they have changed over time.
Where the term "Progressive Web App" came from¶
The June 2015 essay¶
The phrase comes from Alex Russell, then an engineer on Google Chrome, and designer Frances Berriman. Russell described it in an essay on his blog Infrequently Noted published on June 15, 2015, titled Progressive Web Apps: Escaping Tabs Without Losing Our Soul. He wrote that "over dinner last night, Frances and I enumerated the attributes of this new class of applications," and that "Frances called them 'Progressive Open Web Apps' and we both came around to just 'Progressive Apps'." The longer form "Progressive Web Apps" became the standard name within months.
The essay's argument was that two new primitives, service workers and the W3C manifest, finally let websites leave the browser tab without becoming something other than websites. In Russell's words, progressive apps are "just websites that took all the right vitamins. They keep the web's ask-when-you-need-it permission model and add in new capabilities like being top-level in your task switcher, on your home screen, and in your notification tray." The essay ends: "Progressive Apps are our ticket out of the tab, if only we reach for it."
Crucially, the essay framed installation as something a site earns: "Sites that want to send you notifications or be on your home screen have to earn that right over time as you use them more and more. They progressively become 'apps'." For years that idea survived in Chrome's engagement heuristic (the user had to click or tap the page at least once and spend at least 30 seconds on it before beforeinstallprompt fired). Current Chromium has removed that requirement, so the idea now lives on mainly in the rule that powerful permissions are requested in context rather than at install time.
The original list of attributes¶
The essay listed nine attributes. The wording below is abridged from Russell's, with the mechanism each one relied on in 2015:
| Attribute (2015 wording) | 2015 mechanism |
|---|---|
| Responsive: to fit any form factor | CSS media queries, fluid layout |
| Connectivity independent: progressively enhanced with Service Workers to let them work offline | Service worker fetch handling and the Cache API |
| App-like interactions: adopt a Shell + Content application model | App shell cached by the service worker |
| Fresh: transparently always up to date thanks to the Service Worker update process | Byte-for-byte service worker update checks |
| Safe: served via TLS (a Service Worker requirement) | HTTPS-only service workers |
| Discoverable: identifiable as "applications" thanks to W3C Manifests and Service Worker registration scope | Manifest plus crawlable URLs |
| Re-engageable: can access the re-engagement UIs of the OS, e.g. Push Notifications | Push API and Notifications API |
| Installable: to the home screen through browser-provided prompts | Chrome's app install banner |
| Linkable: zero-friction, zero-install, and easy to share | Plain URLs |
How "progressive" joined the list¶
Six months later, on December 15, 2015, Addy Osmani's Chrome developer article Getting started with Progressive Web Apps republished the list with a tenth item at the top: Progressive, defined as working for every user, regardless of browser choice, because such apps are built with progressive enhancement as a core tenet. That ten-item version (progressive, responsive, connectivity independent, app-like, fresh, safe, discoverable, re-engageable, installable, linkable) is the one most people quote today. The History & Evolution page covers what happened before and after in detail.
The ten characteristics, explained technically¶
The original characteristics are often recited as marketing. Each one actually maps to specific mechanisms that you can implement and verify.
Progressive¶
Meaning: the app works for every user in every browser, and improves where the browser supports more.
Mechanism: progressive enhancement and feature detection. The HTML must render meaningful content without JavaScript where possible; JavaScript features must be detected ("serviceWorker" in navigator), never inferred from the user agent string. Registration failures must be caught and ignored.
Verify it: load the site with service workers disabled (Chrome DevTools > Application > Service workers > "Bypass for network", or a browser that lacks a given API) and confirm it still works.
Responsive¶
Meaning: the UI adapts to any viewport, input method and window size, including very narrow phone screens, large desktop windows, split-screen, foldables and resizable app windows.
Mechanism: fluid layout (CSS Grid, Flexbox), container queries, min()/clamp(), the <meta name="viewport" content="width=device-width, initial-scale=1"> tag, env(safe-area-inset-*) for notches in standalone mode, and pointer/hover media queries for touch versus mouse. Installed desktop apps are frequently resized to widths you never see in a browser tab.
Verify it: resize an installed app window down to its minimum width and test with touch, mouse and keyboard. See Responsive & Adaptive Design.
Connectivity independent¶
Meaning: the app starts and does something useful with no network, a slow network or a "lie-fi" connection that appears online but never completes requests.
Mechanism: a service worker fetch handler that serves at least the app shell and an offline fallback page from Cache Storage, network timeouts (a request that hangs is worse than one that fails), and client-side data in IndexedDB. Note that navigator.onLine === true only means the device has a network interface, not that your server is reachable.
Verify it: DevTools network throttling "Offline" and a custom profile with multi-second latency; also test with the server returning 5xx errors.
App-like¶
Meaning: interactions feel like an application: instant navigation between views, no full-page white flashes, appropriate touch targets, no accidental text selection on UI chrome, a window without browser UI.
Mechanism: the app shell model or a well-cached multi-page app, View Transitions for animated navigation (now supported for same-document and cross-document navigations in several engines), the manifest display member (standalone, minimal-ui, fullscreen) or display_override (for example window-controls-overlay), theme_color and background_color for the title bar and splash screen.
Verify it: install the app and compare it side by side with a native app on the same device. See App-Like UX Patterns and View Transitions.
Fresh¶
Meaning: users get updates without visiting a store, and without being stuck on stale versions.
Mechanism: the service worker update algorithm. On every navigation to an in-scope page (and on functional events such as push or sync only if the last check was more than 24 hours ago), the browser re-fetches the service worker script, bypassing the HTTP cache by default (updateViaCache: "imports"), and installs a new version if it differs byte-for-byte, including changes in imported scripts. The new worker waits until no client uses the old one, unless you call skipWaiting(). Freshness is also a manifest concern: browsers periodically re-check the manifest of installed apps and update names, icons and other members according to their own policies.
Verify it: deploy a change and watch DevTools > Application > Service workers for the "waiting to activate" state. See Updating Service Workers and App Identity & Updates.
Safe¶
Meaning: content cannot be tampered with or observed in transit, and powerful features are only available to authenticated origins.
Mechanism: service workers only register in secure contexts, because a worker that can intercept every request of an origin and persist indefinitely would be a devastating man-in-the-middle payload. Most capability APIs are also restricted to secure contexts. Add HSTS, a Content Security Policy and careful scoping of the service worker. See Service Worker Security.
Verify it: window.isSecureContext must be true on every page; check the HSTS header and that no mixed content is reported in the console.
Discoverable¶
Meaning: search engines and users can find the app, and user agents can recognize it as an app.
Mechanism: every screen has a real URL that returns server-rendered or at least crawlable content, plus the manifest, which lets browsers identify the site as installable. PWAs are indexed exactly like any other website; there is no special "PWA ranking". App stores add another discovery channel (see Publishing to App Stores).
Verify it: fetch your URLs with JavaScript disabled and with a crawler user agent; see SEO for PWAs.
Re-engageable¶
Meaning: the app can bring users back through OS-level surfaces.
Mechanism: the Push API (a push subscription with VAPID keys, delivered by the browser vendor's push service to your service worker's push event), the Notifications API (registration.showNotification()), the Badging API (navigator.setAppBadge()), and on Safari 18.4 and later, Declarative Web Push, which displays notifications without running service worker code. On iOS and iPadOS, push is only available to web apps added to the Home Screen.
Verify it: send a test push with the app closed. See Push Notifications and Web Push on iOS & Safari.
Installable¶
Meaning: users can keep the app on their device without an app store.
Mechanism: the browser's installability logic, driven by the manifest. The browser either shows its own install UI (address bar icon, menu item, Share sheet action) or, in Chromium, fires beforeinstallprompt so you can offer installation in your own UI. Installation results in a WebAPK on Android, an OS-registered app on desktop, a Home Screen web app on iOS and iPadOS, or a Dock app on macOS Safari.
Verify it: DevTools > Application > Manifest shows installability errors in Chromium. See Installability Criteria and Install Prompts & Custom UI.
Linkable¶
Meaning: every state worth sharing has a URL; sharing the app is sharing a link.
Mechanism: real routes, the History API for client-side navigation, deep links that work on a cold load, and on the OS side, link capturing so that opening an in-scope link can launch the installed app (WebAPK intent filters on Android; on desktop Chromium, the launch_handler manifest member decides whether a launch reuses an existing app window). A PWA that only works from its start_url has thrown away the web's biggest advantage. See Protocol Handlers & Launch Handling.
Verify it: copy a URL from deep inside the app, open it in a fresh private window and on another device.
Use the characteristics as a test plan
Each "Verify it" line above is a test you can automate or put on a release checklist. The Production Checklist turns them into concrete pass/fail items.
What technically makes a site a PWA today¶
The three technical ingredients¶
1. A secure context. Browsers expose service workers and most capability APIs only when window.isSecureContext is true. In practice that means HTTPS in production and localhost or 127.0.0.1 during development. Installability is stricter than the secure-context definition: file:// URLs count as secure contexts for some purposes but cannot be installed, and localhost is treated as installable only for development convenience.
2. A Web App Manifest. A JSON document linked from every page with <link rel="manifest">. It supplies the app's name, icons, start URL, scope, display mode, colors and optional integrations (shortcuts, share target, file handlers, protocol handlers, screenshots for the rich install UI). The manifest id gives the app a stable identity independent of start_url. The browser fetches and parses the manifest itself; nothing requires you to cache it in the service worker for installation to work, although precaching it does no harm.
3. A service worker (strongly recommended, no longer universally required). A script registered with navigator.serviceWorker.register() that controls pages within its scope. It is what makes the app reliable and re-engageable: offline support, instant repeat loads, push handling, background sync. Chrome originally required a service worker with a fetch handler for installation; it removed that requirement for installation from the menu in Chrome 108 on Android and Chrome 112 on desktop, and shows a default offline page for installed apps that do not provide their own. Safari has never required a service worker for Home Screen web apps.
What each browser actually requires¶
The detailed, maintained version of this table is on Installability Criteria; this summary is enough to understand the landscape.
| Browser / platform | How users install | What the site must provide | Programmatic prompt |
|---|---|---|---|
| Chrome, Edge (desktop) | Address bar install icon, menu | HTTPS; manifest with name or short_name, at least one purpose: any icon of 144 px or larger (192 px and 512 px recommended), start_url, display (or display_override) resolving to standalone, fullscreen or minimal-ui; not already installed. Any page can also be installed manually via "Install page as app" (Chrome 128+) | ✅ beforeinstallprompt |
| Chrome (Android) | Install prompt, menu "Add to home screen > Install" | Same manifest criteria (no engagement requirement in current Chromium); creates a WebAPK on devices with Google Play services | ✅ beforeinstallprompt |
| Samsung Internet | Address bar install icon, menu | Manifest criteria similar to Chrome's | ✅ beforeinstallprompt |
| Safari (iOS and iPadOS 26) | Share sheet > Add to Home Screen, "Open as Web App" toggle | Nothing: every site can be opened as a web app; the manifest customizes the result | ❌ |
| Safari (macOS Sonoma and later) | File > Add to Dock | Nothing: any site works; the manifest customizes the result | ❌ |
| Firefox (Windows, 143+) | Pin a site to the taskbar as a web app | Any site (the Microsoft Store build of Firefox gained support in Firefox 150) | ❌ |
| Firefox (Android) | Menu > Add to Home screen / Install | Manifest recommended; creates a shortcut rather than a WebAPK | ❌ |
Support data as of September 2026. For live data see MDN: Making PWAs installable and caniuse: Web App Manifest.
Experimental
Chromium is experimenting with site-initiated installation: navigator.install() (the Web Install API) and a declarative <install> element. Both ran as origin trials in Chrome and Edge during 2025–2026, and an Intent to Ship posted in September 2026 targets Chrome 156 on desktop. WebKit has publicly opposed site-initiated installation and Mozilla has not taken a position, so treat these as Chromium-only progressive enhancements. See Install Prompts & Custom UI.
What is marketing, not a requirement¶
Many things are routinely described as "what makes a PWA" but are not required by any browser:
- A single-page application architecture. Multi-page apps are fully valid PWAs and often simpler to make reliable. See SPA vs MPA PWAs.
- A framework or a specific build tool. Workbox, Vite PWA and framework plugins are conveniences.
- A Lighthouse "PWA" badge. Lighthouse removed its PWA category in version 12 (2024). See Lighthouse & Auditing.
- An app store listing. Optional distribution channel, not a definition.
- Push notifications. Many excellent PWAs never send one.
- "Works fully offline." A branded offline page is the useful minimum; full offline data sync is an architectural choice, not a badge.
- A splash screen. Browsers generate one from
name,background_colorand icons on some platforms; its absence does not disqualify anything.
Progressive enhancement: the core principle¶
Progressive enhancement means you build a baseline that works everywhere, then layer on improvements that are applied only when the browser supports them. For PWAs this is not just philosophy; it is the only workable strategy, because the capability matrix across Chromium, WebKit and Gecko (and across desktop and mobile within each) is too fragmented for any other approach.
Three rules make it work in practice:
- Detect features, not browsers. User-agent sniffing breaks as soon as a browser ships a feature (which happens every four weeks in Chromium and several times a year in Safari). Every iOS browser uses WebKit, so "Chrome on iOS" does not have Chrome's capabilities.
- Detect at the point of use, and handle failure there too. A feature can be present but fail at runtime: a permission can be denied, a user activation can be missing, a quota can be exceeded, or an enterprise policy can block it.
- Hide or adapt UI for unsupported features. Don't show a "Share" button that throws, or an "Install" button that can never work.
A feature-detection module¶
The excerpt below shows the pattern: presence tests on interfaces (never user-agent checks), the current display mode, and data attributes so CSS can adapt without JavaScript branching everywhere. The full per-capability detection table, including the checks for storage, file handling and window controls overlay, is on Capabilities; display-mode detection edge cases are covered in Display Modes.
// Presence does not guarantee success: permissions, user activation and platform
// policy can still make a call fail, so callers must handle errors.
export function detectCapabilities() {
return {
serviceWorker: "serviceWorker" in navigator,
push: "PushManager" in window,
badging: "setAppBadge" in navigator,
backgroundSync: "SyncManager" in window, // Chromium-only today
installPromptEvent: "onbeforeinstallprompt" in window, // Chromium
share: "share" in navigator,
fileSystemAccess: "showOpenFilePicker" in window,
};
}
export function getDisplayMode() {
// Check the non-standard flag first: iOS web apps with display "standalone"
// match (display-mode: fullscreen), and macOS Safari Dock apps also set it.
if (navigator.standalone === true) return "standalone";
for (const mode of ["fullscreen", "standalone", "minimal-ui", "window-controls-overlay"]) {
if (matchMedia(`(display-mode: ${mode})`).matches) return mode;
}
return "browser";
}
export function applyCapabilityAttributes(root = document.documentElement) {
const caps = detectCapabilities();
for (const [name, supported] of Object.entries(caps)) {
// Produces e.g. data-cap-share="false" for CSS hooks.
root.dataset[`cap${name[0].toUpperCase()}${name.slice(1)}`] = String(supported);
}
root.dataset.displayMode = getDisplayMode();
return caps;
}
Using it: CSS hides unsupported controls, and each feature call is still guarded with try/catch because detection only proves the API exists.
/* Hide controls whose capability is missing. The baseline UI never depends on them. */
[data-cap-share="false"] .share-button,
[data-cap-badging="false"] .badge-settings,
[data-cap-file-system-access="false"] .open-local-file {
display: none;
}
/* Standalone apps have no browser back button: show an in-app one. */
.in-app-back { display: none; }
[data-display-mode="standalone"] .in-app-back,
[data-display-mode="window-controls-overlay"] .in-app-back {
display: inline-flex;
}
import { applyCapabilityAttributes } from "./capabilities.js";
const caps = applyCapabilityAttributes();
document.querySelector(".share-button")?.addEventListener("click", async () => {
if (!caps.share) return;
const data = { title: document.title, url: location.href };
try {
// navigator.share() requires transient user activation (this click),
// and rejects with AbortError if the user dismisses the share sheet.
await navigator.share(data);
} catch (error) {
if (error.name === "AbortError") return; // User cancelled: not an error.
// NotAllowedError: no user activation or blocked by permissions policy.
// Fall back to copying the link.
await navigator.clipboard?.writeText(data.url).catch(() => {});
}
});
Never gate core functionality on a service worker
A service worker can be absent for many legitimate reasons: the first visit, a private window with storage restrictions, an enterprise policy, a browser that evicted your registration under storage pressure, a user who cleared site data, or a registration that failed. If your app breaks without the worker, it is not progressive and will fail for real users. The first visit to any PWA is always served by the network.
How an installed PWA actually runs¶
Installing a PWA does not install a new runtime. The installed app runs on the same browser engine that installed it: Blink in Chrome, Edge and Samsung Internet; WebKit in Safari (and in every iOS browser); Gecko in Firefox. What changes is the window, how the OS launches it, and on some platforms where its data lives.
The launch sequence¶
sequenceDiagram
participant User
participant OS as OS launcher
participant Browser as Browser engine
participant SW as Service worker
participant Net as Network
User->>OS: Tap or click app icon
OS->>Browser: Launch app id or start_url
Browser->>Browser: Create standalone window for the app scope
Browser->>SW: Start worker if registered and not running
Browser->>SW: fetch event for start_url navigation
alt Response in cache
SW-->>Browser: Cached app shell
else Not cached
SW->>Net: fetch(request)
Net-->>SW: Response
SW-->>Browser: Network response
end
Browser->>User: First paint in app window The service worker is started on demand (it is not a daemon) and handles the navigation to start_url like any other navigation. That is why a cached app shell makes installed PWAs start as fast as native apps, and why an app without a service worker needs the network to start.
Standalone windows and display modes¶
The manifest display member requests a presentation mode; the browser may fall back along the chain fullscreen → standalone → minimal-ui → browser. display_override lets you request modes that are not in the chain, such as window-controls-overlay (the app draws into the title bar area on desktop Chromium) or tabbed (experimental). CSS can react to the actual mode with the display-mode media feature.
In a standalone window there is no address bar, no back button and no reload button. Your UI must provide back navigation where needed, and keyboard shortcuts such as Alt+Left or Cmd+[ still work on desktop. When the user navigates outside the manifest scope, browsers show some form of browser UI so the user can see which origin they are on; Chromium desktop, for example, displays a toolbar with the origin and a close button. See Display Modes.
Process model by platform¶
| Platform | What "installing" creates | What runs the app |
|---|---|---|
| Chrome / Edge on Windows | A Start menu shortcut and a registered app entry that can be uninstalled from Windows Settings | The shortcut launches the browser's proxy executable (chrome_proxy.exe, msedge_proxy.exe) with an --app-id. The app window is a browser window of the "app" type inside the normal browser process; renderers follow the browser's site-isolation rules |
| Chrome / Edge on macOS | An app shim bundle (for Chrome, in ~/Applications/Chrome Apps.localized/) | The shim forwards the launch to the browser, which opens the app window; the app gets its own Dock icon and appears in the app switcher |
| Chrome on Linux and ChromeOS | A .desktop launcher entry (Linux) or a launcher and shelf entry (ChromeOS) | The browser process, as on Windows |
| Chrome / Samsung Internet on Android | A WebAPK: a generated, signed Android package that appears in the launcher and Android settings and registers intent filters for the app's URLs | The installing browser. The WebAPK is a thin shell that starts the browser's web app activity, shown as its own task in Recents |
| Android app store (Trusted Web Activity) | An APK published to Google Play or another store | The user's browser that supports TWA (usually Chrome), verified via Digital Asset Links. See Trusted Web Activity |
| iOS / iPadOS | A Home Screen web app | A system-managed WebKit web app container, separate from the Safari app. Web apps added from other iOS browsers (possible since iOS 16.4) also run on WebKit |
| Safari on macOS | A web app in the Dock and Launchpad | A separate app process with its own window, managed by Safari's web app support |
| Firefox on Windows | A taskbar-pinned web app | Firefox, as a simplified window with your installed add-ons available |
Two consequences matter for engineering:
- On Chromium platforms, the app and the browser are the same process tree. Crashing, updating or quitting the browser affects the app. Browser-level settings (site permissions, cookies, extensions running on the page) apply to the app too.
- On Android, the WebAPK is a launcher and an OS registration, not a runtime. The code, storage and service worker live in the browser. Uninstalling the WebAPK removes the app entry, but you should not assume it clears the site's data in the browser profile, and clearing the browser's data does affect the installed app.
Storage: shared with the browser or separate?¶
Where an installed app's data lives is the single most surprising platform difference, because it decides whether a user who logged in on the website is still logged in when they open the installed app.
| Platform | Storage relationship | Practical effect |
|---|---|---|
| Chrome / Edge desktop | Shared with the browser profile that installed the app | Log in once in a tab and the app is logged in; clearing site data in the browser clears the app |
| Chrome Android (WebAPK) | Shared with Chrome's current profile: cookies, IndexedDB, Cache Storage and service worker registrations | Same as desktop |
| Trusted Web Activity | Shared with the browser providing the TWA | Same as the browser |
| iOS / iPadOS Home Screen web app | Isolated: each web app has its own cookies, localStorage, IndexedDB, Cache Storage and service worker registrations, separate from Safari and from other web apps | Since iOS 17.2, Safari's cookies are copied into the web app when it is created, so users generally stay signed in; localStorage, IndexedDB and caches written in Safari are not copied |
| Safari on macOS (Dock app) | Isolated after install: Safari copies the site's cookies into the web app when it is created; no other data is copied and nothing is shared afterwards | Users stay logged in at install time, but sessions then diverge |
Two WebKit details follow from the isolation. First, Safari's Intelligent Tracking Prevention deletes script-writable storage for sites that go seven days of Safari use without user interaction, but WebKit states that Home Screen web apps "have their own counter of days of use" which matches actual use of the web app. Second, push subscriptions belong to whichever storage partition created them: on iOS and iPadOS only the Home Screen web app can subscribe at all, and on macOS a subscription made in a Safari tab and one made in the Dock web app for the same site are separate subscriptions that your server must treat as two devices.
Design authentication for isolated storage
Assume the installed app may start with empty storage. Offer passkeys or another low-friction sign-in inside the app, keep the login page within the manifest scope so it does not open in a browser sheet, and do not rely on a cookie set in the browser before installation (except on platforms where you have verified sharing). See Authentication & Passkeys and Privacy & Storage Partitioning.
The app can be stopped at any time¶
An installed PWA is subject to the same lifecycle as a browser tab, plus the OS's app lifecycle. On mobile, the OS may kill the backgrounded app (and the browser process hosting it) to reclaim memory; browsers freeze and discard background tabs and app windows; service workers are terminated when idle and restarted for the next event. Persist state early (IndexedDB), restore it on launch, and listen to the Page Lifecycle visibilitychange event rather than unload, which is unreliable on mobile and deprecated in Chromium.
Anatomy of a minimal PWA¶
The smallest useful PWA has five files: a page, a stylesheet or script, a manifest, a service worker and an offline fallback page, plus icons. Everything below is complete, production-grade code you can deploy as-is; the tutorial builds a larger app step by step.
/
├── index.html
├── offline.html
├── app.js
├── styles.css
├── manifest.webmanifest
├── sw.js
└── icons/
├── icon-192.png
├── icon-512.png
├── maskable-512.png
└── apple-touch-icon.png (180×180, used by iOS for the Home Screen icon)
The HTML head¶
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- viewport-fit=cover lets standalone apps draw under notches; pad with env(safe-area-inset-*) -->
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Field Notes</title>
<meta name="description" content="Offline-capable notes that sync when you are back online.">
<!-- The manifest: this single tag is what makes the site installable. -->
<link rel="manifest" href="/manifest.webmanifest">
<!-- Title bar / status bar color; the media variants follow the OS color scheme. -->
<meta name="theme-color" content="#3c0a86" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#1b0540" media="(prefers-color-scheme: dark)">
<!-- Icons: iOS and iPadOS still prefer apple-touch-icon for the Home Screen icon. -->
<link rel="icon" href="/icons/icon-192.png" type="image/png" sizes="192x192">
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
<link rel="stylesheet" href="/styles.css">
<!-- type="module" scripts are deferred: registration will not block rendering. -->
<script type="module" src="/app.js"></script>
</head>
<body>
<header class="app-bar">
<button class="in-app-back" type="button" hidden>Back</button>
<h1>Field Notes</h1>
</header>
<main id="content">
<!-- Server-rendered content goes here, so the page works without JavaScript. -->
<p>Your notes will appear here.</p>
</main>
<div id="update-banner" role="status" hidden>
A new version is available. <button type="button" id="reload-button">Reload</button>
</div>
</body>
</html>
Notes on what is deliberately absent:
<meta name="apple-mobile-web-app-capable" content="yes">is not needed. Safari honors the manifestdisplaymember for Home Screen web apps, and since iOS 26 every site added to the Home Screen opens as a web app by default. Chromium logs a deprecation warning for this tag.apple-touch-startup-imagesplash screens are optional polish; see Splash Screens & Theming.- No
<noscript>"please enable JavaScript" wall: server-render meaningful HTML instead.
The manifest¶
{
"id": "/",
"name": "Field Notes",
"short_name": "Notes",
"description": "Offline-capable notes that sync when you are back online.",
"start_url": "/?source=pwa",
"scope": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3c0a86",
"lang": "en",
"dir": "ltr",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
{
"src": "/icons/maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
What each member is doing here:
| Member | Why it is set this way |
|---|---|
id | A stable app identity. Without it, the identity is derived from start_url, so changing start_url later would create a different app for browsers that key installs on the id. It resolves against the start_url origin, so "/" means https://example.com/ |
name, short_name | name is used in install dialogs and app lists; short_name under launcher icons where space is tight |
start_url | The URL opened on launch. The query string lets analytics distinguish app launches; it must be within scope and same-origin |
scope | Navigations inside this path stay in the app window; outside it, browser UI appears. Defaults to the directory of start_url if omitted |
display | standalone gives an app window without browser UI. Chromium requires standalone, fullscreen or minimal-ui for installability promotion. window-controls-overlay is not a valid display value (it is ignored); use it only in display_override |
background_color | Splash screen and window background before CSS loads; should match your page background |
theme_color | Title bar and task switcher color. The <meta name="theme-color"> tag in the page overrides it on platforms that support it |
icons | Chromium's installability floor is one purpose: any icon of at least 144 px (PNG, SVG or WebP); 192 px and 512 px are recommended. A separate maskable icon avoids white boxes around the icon on Android adaptive icon shapes |
Serve the file with Content-Type: application/manifest+json. Browsers are tolerant of application/json, but the registered media type avoids warnings in some tooling. The full member reference is in Members Reference.
Registering the service worker¶
import { applyCapabilityAttributes } from "./capabilities.js";
applyCapabilityAttributes();
async function registerServiceWorker() {
if (!("serviceWorker" in navigator)) return; // Progressive: no worker, no problem.
try {
const registration = await navigator.serviceWorker.register("/sw.js", {
scope: "/",
// "none" also bypasses the HTTP cache for importScripts() during update checks.
updateViaCache: "none",
});
// A worker that is already waiting when the page loads (e.g. user ignored the banner earlier).
if (registration.waiting && navigator.serviceWorker.controller) {
offerUpdate(registration.waiting);
}
// A new worker found while this page is open.
registration.addEventListener("updatefound", () => {
const incoming = registration.installing;
if (!incoming) return;
incoming.addEventListener("statechange", () => {
// "installed" + an existing controller = an update, not the first install.
if (incoming.state === "installed" && navigator.serviceWorker.controller) {
offerUpdate(incoming);
}
});
});
// Long-lived app windows (installed PWAs are often open for days) never navigate,
// so they would never trigger the browser's own update check. Check periodically.
setInterval(() => registration.update().catch(() => {}), 60 * 60 * 1000);
} catch (error) {
// Registration can fail for many reasons (storage disabled, script 404, syntax error).
// The site must keep working.
console.warn("Service worker registration failed:", error);
}
}
function offerUpdate(worker) {
const banner = document.getElementById("update-banner");
const button = document.getElementById("reload-button");
if (!banner || !button) return;
banner.hidden = false;
button.addEventListener(
"click",
() => {
// Ask the waiting worker to activate; reload once it has taken control.
worker.postMessage({ type: "SKIP_WAITING" });
},
{ once: true },
);
}
// Reload exactly once when a new worker takes control, so the page and worker versions match.
let reloading = false;
navigator.serviceWorker?.addEventListener("controllerchange", () => {
if (reloading) return;
reloading = true;
window.location.reload();
});
registerServiceWorker();
The SKIP_WAITING handshake exists because a new service worker waits by default until every tab and app window using the old one has closed, which for an installed PWA may be never. Letting the user choose when to reload avoids swapping code under a page mid-task. The trade-offs between this and other update patterns are covered in Updating Service Workers.
The service worker¶
The excerpt below shows the three moves every PWA worker makes: precache the shell on install, delete old caches on activate, and answer navigations network-first with an offline fallback. The complete, commented worker (with stale-while-revalidate for assets, navigation preload, a network timeout and the SKIP_WAITING message handler) is built step by step in Build Your First PWA; the strategies themselves are compared in Caching Strategies.
const VERSION = "2026-09-25.1"; // Bump on every deploy that changes precached files.
const SHELL_CACHE = `shell-${VERSION}`;
const RUNTIME_CACHE = "runtime-v1";
const OFFLINE_URL = "/offline.html";
const SHELL_ASSETS = ["/", OFFLINE_URL, "/styles.css", "/app.js", "/manifest.webmanifest"];
self.addEventListener("install", (event) => {
// addAll() is atomic: one failed request fails the install and the old worker stays.
event.waitUntil(
caches.open(SHELL_CACHE).then((cache) =>
cache.addAll(SHELL_ASSETS.map((url) => new Request(url, { cache: "reload" }))),
),
);
});
self.addEventListener("activate", (event) => {
const keep = new Set([SHELL_CACHE, RUNTIME_CACHE]);
event.waitUntil(
caches.keys().then((names) =>
Promise.all(names.filter((n) => !keep.has(n)).map((n) => caches.delete(n))),
),
);
});
self.addEventListener("fetch", (event) => {
if (event.request.mode !== "navigate") return; // Assets: see first-pwa.md.
event.respondWith(
fetch(event.request).catch(async () =>
(await caches.match(event.request, { ignoreSearch: true })) ??
(await caches.match(OFFLINE_URL)),
),
);
});
Why the worker is shaped this way:
- Install precaches only the shell. Precaching everything slows the first visit and wastes quota; runtime caching picks up the rest as the user browses. See Precaching & Runtime Caching.
- HTML is network-first (the full worker adds a timeout); static assets are cache-first or stale-while-revalidate. Mixing strategies per request type is normal. See Caching Strategies.
- The runtime cache is not versioned, so previously visited pages remain available offline after an update, while the shell cache is replaced atomically.
- In the full worker,
event.waitUntil()wraps background cache writes so the browser does not terminate the worker mid-write, and navigation preload removes worker boot-up time from the critical path. See Navigation Preload.
The offline page¶
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Offline · Field Notes</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<main>
<h1>You are offline</h1>
<p>This page has not been saved for offline use yet. Pages you have opened before are still
available, and notes you write are saved on this device and sync when you reconnect.</p>
<button type="button" onclick="location.reload()">Try again</button>
</main>
</body>
</html>
Serving requirements¶
Two HTTP details trip up almost every first deployment:
/sw.js
Cache-Control: no-cache
Content-Type: text/javascript; charset=utf-8
/manifest.webmanifest
Content-Type: application/manifest+json
Cache-Control: no-cache
- Do not far-future cache
sw.js. Browsers bypass the HTTP cache for the service worker script during update checks by default (updateViaCache: "imports", the default since Chrome 68); the old 24-hour HTTP cache cap only matters if you opt intoupdateViaCache: "all". CDNs and intermediate caches do not know any of that.no-cache(revalidate every time) is the safe choice. - Serve the worker with a JavaScript MIME type. Registration fails with a
SecurityErrorif the script is served astext/htmlortext/plain, which is a common symptom of a single-page-app fallback rule returningindex.htmlfor/sw.js. - Keep the worker at the root (or send a
Service-Worker-Allowedheader) if it must control pages outside its own directory. A worker at/js/sw.jscan only control/js/by default. See Registration & Scope.
Verifying the result¶
In Chrome or Edge DevTools, open Application: the Manifest pane shows the parsed manifest and any installability problems; Service workers shows the active, waiting and installing versions; Cache storage lists the caches the worker created. Toggle Offline in the Network panel and reload to confirm the fallback works. In Safari, use Web Inspector connected to the device (or the Develop menu for Dock apps). See Browser DevTools.
What PWAs can do today¶
The capability surface of the web has grown enormously since 2015, but it is not uniform. The table summarizes the major capability families and where they work; every row links to a page with full API details and precise version data.
| Capability | APIs | Availability summary | Details |
|---|---|---|---|
| Offline and instant loading | Service Worker, Cache Storage | ✅ All modern browsers | Caching & Offline |
| Structured and file storage | IndexedDB, Origin Private File System, StorageManager | ✅ All modern browsers (quota rules differ) | Storage Quotas |
| Push notifications | Push API, Notifications API | ✅ Chromium, Firefox, Safari on macOS; ⚠️ iOS/iPadOS 16.4+ only for Home Screen web apps | Push Notifications |
| Push without service worker code | Declarative Web Push (window.pushManager) | ⚠️ Safari only: iOS/iPadOS 18.4+ Home Screen apps, macOS Safari 18.5+ | Web Push on iOS & Safari |
| App icon badges | Badging API | ⚠️ Chromium desktop; installed web apps in Safari (iOS/iPadOS 16.4+, macOS Sonoma+) | Badging API |
| Deferred and background work | Background Sync, Periodic Background Sync, Background Fetch | ⚠️ Chromium only; Periodic Sync requires installation | Background Sync |
| Share to other apps | Web Share API | ✅ Safari, Chromium on Android and most desktop platforms, Firefox for Android | Web Share API |
| Receive shares | Web Share Target (share_target) | ⚠️ Chromium, installed apps only (primarily Android and ChromeOS) | Web Share Target |
| Read and write local files | File System Access | ⚠️ Chromium desktop | File System Access |
| Open files from the OS | File Handling (file_handlers, launchQueue) | ⚠️ Chromium desktop, installed apps only | File Handling |
| Custom URL schemes | registerProtocolHandler(), protocol_handlers | ⚠️ registerProtocolHandler() in Chromium and Firefox; manifest protocol_handlers in Chromium desktop | Protocol Handlers |
| Custom title bar | Window Controls Overlay | ⚠️ Chromium desktop, installed apps only | Window Controls Overlay |
| Hardware access | Web Bluetooth, WebUSB, Web Serial, WebHID, Web NFC | ⚠️ Mostly Chromium-only (platform coverage varies per API); Firefox 151 on desktop ships Web Serial, gated behind a site-permission add-on | Hardware & Device APIs |
| Payments | Payment Request API, Apple Pay on the web | ✅ Chromium and Safari; ❌ Firefox | Payments |
| Passwordless sign-in | WebAuthn / passkeys | ✅ All modern browsers | Authentication & Passkeys |
| Media integration | Media Session, Screen Wake Lock, Picture-in-Picture | ✅ Broadly supported (Document Picture-in-Picture is Chromium only) | Media & System APIs |
Support data as of September 2026. For live per-version data, check the linked pages, MDN and caniuse.com.
⚠️ = supported with platform or browser restrictions described in the row.
Hard limits: what PWAs still cannot do¶
Being honest about limits is what makes a PWA decision defensible. As of September 2026:
No general-purpose background execution. A service worker only runs in response to events (fetch, push, notification click, sync, periodic sync, background fetch) and is terminated when idle. Browsers impose time limits on event handling, and there is no equivalent of a native background service, geofencing, or continuous background location. Periodic Background Sync (Chromium only) runs at intervals the browser chooses based on site engagement, not the interval you request.
No programmatic install prompt outside Chromium. Safari and Firefox never fire beforeinstallprompt. On iOS and iPadOS, users install from the Share sheet; your only lever is clear in-app instructions. The experimental Web Install API is Chromium-only and opposed by WebKit.
Capability gaps are structural, not temporary, for some APIs. Mozilla and Apple have published negative standards positions on several hardware APIs (for example WebUSB and Web Bluetooth) on privacy and security grounds, so do not plan on those arriving in Firefox or Safari. Design those features as Chromium-only enhancements.
iOS and iPadOS restrictions. Every iOS browser uses WebKit, so capabilities are bounded by Safari's. Push requires a Home Screen web app and a user gesture to request permission. Storage is isolated per web app. There is no Background Sync, Periodic Sync or Background Fetch. Web apps cannot be distributed through the App Store as-is: store distribution requires wrapping the site in a native app that meets Apple's review guidelines. See iOS & iPadOS.
Storage is not guaranteed. Unless the origin is granted persistent storage via navigator.storage.persist(), data is "best effort" and can be evicted under storage pressure. Safari additionally deletes script-writable storage of websites (not Home Screen web apps) after seven days of Safari use without user interaction. See Storage Quotas & Persistence.
No raw networking or privileged OS access for ordinary PWAs. TCP/UDP sockets (Direct Sockets), embedding arbitrary sites with full control (Controlled Frame) and similar high-trust APIs are only available to Isolated Web Apps, a signed and bundled app model currently limited to enterprise-managed Chromium environments. See Isolated Web Apps.
No deep OS surfaces on most platforms. Home screen widgets, system-wide keyboard extensions, lock screen integrations beyond notifications, Siri or Assistant intents and similar surfaces are not available to web apps. App shortcuts (the shortcuts manifest member) are the main exception, and they are supported on Chromium platforms but not in Safari.
The engine decides. An installed PWA cannot pick its browser engine or version. If the user's browser lacks an API, so does your app, and an installed app is only as up to date as the browser that runs it.
Common misconceptions¶
"A PWA is a framework or a library"¶
It is neither. You can build a PWA with no dependencies at all (the example above has none) or with any framework. Tools like Workbox, the Vite PWA plugin or framework integrations generate service workers and manifests for you, but the result is the same set of standard files.
"PWAs must be single-page apps"¶
Service workers were designed around navigation requests, and multi-page apps benefit from them at least as much as SPAs: each navigation can be served from cache, streamed, or composed from cached partials. Cross-document View Transitions have also removed much of the "page flash" argument for SPAs. The choice between SPA and MPA is an architectural one that is independent of being a PWA. See SPA vs MPA PWAs and Streaming Responses.
"PWAs are a Google-only technology"¶
The term was popularized by Google engineers, and Chromium implements the most capability APIs, but the core standards are implemented by all major engines. Service workers shipped in Chrome 40 (2015), Firefox 44 (2016) and Safari 11.1 (2018). Microsoft shipped PWA support in EdgeHTML in 2018 and was the first to list PWAs in an OS app store. Apple added Web Push and badging for Home Screen web apps in iOS 16.4, Add to Dock in macOS Sonoma, Declarative Web Push in Safari 18.4, and made every Home Screen site a web app in iOS 26. The Web Application Manifest specification's current editors come from Apple, Google and Thinktecture.
"PWAs are only for mobile"¶
Desktop is where installed PWAs are often most compelling: Chromium browsers support installation on Windows, macOS, Linux and ChromeOS (Chrome completed desktop support in 2019), Safari on macOS Sonoma and later can add any site to the Dock, and Firefox 143 added taskbar web apps on Windows. Desktop also has the largest capability surface (File System Access, File Handling, Window Controls Overlay, most hardware APIs). See Desktop Platforms.
"PWAs are about working offline"¶
Offline support is one benefit among several, and for many apps it is a minor one. A service worker's bigger practical wins are usually speed (instant repeat loads from cache, navigation preload, streaming) and resilience on slow or flaky networks. Plenty of valuable PWAs are online-first with a simple offline fallback page. Conversely, "offline-first" data synchronization is a significant architectural commitment. See Offline-First Data & Sync.
"You need a service worker to be installable"¶
Not anymore. Chrome removed the requirement for menu installation in Chrome 108 (Android) and 112 (desktop); Safari never had it; Firefox's Windows web apps work with any site. You still want a service worker for everything that makes an installed app good: offline start, speed, push and background events.
"PWAs cannot be published in app stores"¶
They can. Google Play accepts PWAs packaged as Trusted Web Activities; the Microsoft Store accepts PWAs directly; other stores, such as the Meta Quest store, accept packaged PWAs too. Apple's App Store requires a native wrapper that satisfies its review guidelines. See Publishing to App Stores and PWABuilder.
"Lighthouse dropped PWAs, so PWAs are dead"¶
Lighthouse 12.0 (released April 2024) removed its PWA category because Chrome's installability criteria had changed and the checks no longer matched them. The underlying APIs were not deprecated; Apple, Google, Microsoft and Mozilla have all shipped new web app features since. See Lighthouse & Auditing for how to audit a PWA today, and the FAQ for more myths and answers.
Well-known examples¶
Public, sourced examples show the range of what "PWA" covers. The Case Studies page collects more, with methodology notes.
| App | Year | What makes it notable |
|---|---|---|
| Flipkart Lite | 2015 | Generally cited as the first large production PWA. Flipkart had shut down its mobile website for an app-only strategy and returned to the web with service workers and Add to Home Screen. The web.dev case study reports 3x more time on site, a 40% higher re-engagement rate and 70% greater conversion among users arriving via the home screen icon |
| Twitter Lite | 2017 | Became Twitter's default mobile web experience worldwide in 2017. The web.dev case study reports a 65% increase in pages per session, 75% more Tweets sent and a 20% decrease in bounce rate, and roughly 600 KB over the wire versus 23.5 MB to install the native Android app |
| Squoosh | 2018 | Google Chrome Labs' image compressor: an installable, fully offline-capable app doing heavy WebAssembly work client-side, demonstrating that "real" desktop-class tools can be PWAs |
| This site | 2026 | progressivewebapps.com is itself installable and readable offline. See This Site Is a PWA |
Beyond individual apps, most large web applications you use daily (mail clients, office suites, chat, music and video streaming, maps, design tools) ship a manifest and a service worker and can be installed from Chromium's address bar today, whether or not they market themselves as PWAs. That is the point of the model: being a PWA is a set of enhancements to a website, not a product category.
Further reading¶
On this site
- History & Evolution: from iPhone web apps and AppCache to the Web Install API
- Core Building Blocks: the manifest, service worker and storage as one system
- Installability Criteria: per-browser install rules and debugging
- PWA vs Native vs Hybrid: trade-offs and decision criteria
- Tutorial: Your First PWA: build a complete app step by step
- Service Worker Lifecycle
- Installation by Platform
- Glossary
External references
- Progressive Web Apps: Escaping Tabs Without Losing Our Soul, Alex Russell (2015)
- Getting started with Progressive Web Apps, Addy Osmani (2015)
- What is a progressive web app? (MDN)
- Making PWAs installable (MDN)
- Web Application Manifest (W3C)
- Service Workers (W3C)
- Revisiting Chrome's installability criteria (Chrome for Developers)
- WebKit Features in Safari 26.0 (WebKit blog)
- WebKit Features in Safari 17.0: web apps on Mac (WebKit blog)