Getting started
Published
At the end of this guide an ordinary website is recognised as installable by Chrome, Edge, and Samsung Internet, opens in its own window from the home screen or dock, and serves its shell from cache. The two files that make the difference are a web app manifest and a service worker; the rest of the site is unchanged.
You need a site served over HTTPS (or from localhost during development), two square
PNG icons at 192 × 192 and 512 × 512 pixels, and a text editor. No framework or build
step is required. In this guide we add the files to the root of the site so that the
manifest scope and the service worker scope both cover every page.
Confirm the origin is secure
Section titled “Confirm the origin is secure”Service workers only register on a secure context. MDN’s service worker guide states
that localhost counts as a secure origin, so a local dev server on
http://localhost:8080 works; a LAN address such as http://192.168.1.5 does not.
Open the site, and in Chrome check that the address bar shows no “Not secure” label
before continuing. Chrome’s installability checker reports the exact failure
Page isn't served from a secure origin when this step is skipped (string from the
DevTools front-end source, AppManifestView.ts).
Write the manifest
Section titled “Write the manifest”Create /manifest.webmanifest at the site root. The members below are the ones
Chromium browsers require for install, per MDN’s installability guide: name or
short_name, icons with a 192 px and a 512 px entry, start_url, and display.
id and scope are optional for install but fixing them now avoids a second identity
later: MDN documents that id defaults to start_url when absent, so a later change
to start_url would otherwise register a second app.
{ "id": "/", "name": "Field Notes", "short_name": "Notes", "description": "Offline-first notes that sync when you are back online.", "start_url": "/?source=pwa", "scope": "/", "display": "standalone", "background_color": "#ffffff", "theme_color": "#1f4e79", "icons": [ { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { "src": "/icons/maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ]}The maskable icon is a separate file with the artwork inside a central circle whose
radius is 40 % of the width; web.dev’s maskable-icon article advises against reusing
one image for both any and maskable because the padding a maskable icon needs
makes the any rendering smaller. Link the manifest from every HTML page:
<link rel="manifest" href="/manifest.webmanifest"><meta name="theme-color" content="#1f4e79">Write and register the service worker
Section titled “Write and register the service worker”Create /sw.js at the site root. The worker below precaches the application shell on
install, deletes caches from earlier versions on activate, and answers navigations
and shell requests from the cache before touching the network. Bump VERSION whenever
a precached file changes; the browser treats a byte-different worker as an update.
const VERSION = 'v1';const SHELL_CACHE = `shell-${VERSION}`;const SHELL = ['/', '/index.html', '/app.css', '/app.js', '/offline.html', '/icons/icon-192.png'];
self.addEventListener('install', (event) => { event.waitUntil(caches.open(SHELL_CACHE).then((cache) => cache.addAll(SHELL)));});
self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then((keys) => Promise.all(keys.filter((key) => key !== SHELL_CACHE).map((key) => caches.delete(key))) ) );});
self.addEventListener('fetch', (event) => { const { request } = event; if (request.method !== 'GET') return; event.respondWith( caches.match(request).then((cached) => { if (cached) return cached; return fetch(request).catch(() => { if (request.mode === 'navigate') return caches.match('/offline.html'); return Response.error(); }); }) );});Register it from the page script. The in check keeps the page working in a browser
without the API; the scope option is shown explicitly although / is the default for
a worker served from the root:
if ('serviceWorker' in navigator) { window.addEventListener('load', async () => { try { const registration = await navigator.serviceWorker.register('/sw.js', { scope: '/' }); console.log('Service worker registered with scope', registration.scope); } catch (error) { console.error('Service worker registration failed:', error); } });}A service worker is no longer a condition for installing from Chrome’s menu: the Chrome
team removed the fetch-handler requirement in Chrome 108 on Android and Chrome 112 on
desktop, and Chrome 112 ignores empty fetch handlers. The automatic install prompt
(beforeinstallprompt) still checks for a fetch handler, which the worker above has.
Check the result in Chrome DevTools
Section titled “Check the result in Chrome DevTools”Open DevTools (Command+Shift+P or Control+Shift+P, type application, choose Show
Application) and select Manifest. The pane lists the parsed Identity,
Presentation, and Icons sections; an Installability section appears only
when something is wrong. Under Service workers the Status line for /sw.js should read
#N activated and is running (N is the worker version number), and Cache Storage should list
shell-v1 with the precached files. Reload once if the cache does not appear:
Chrome’s DevTools documentation notes that the first write to a new cache may not be
detected until the page is reloaded.
Install and launch it
Section titled “Install and launch it”On desktop Chrome the address bar shows an install icon on an installable page;
otherwise use More > Cast, save, and share > Install page as app… (English
UI). On Android the path is More > Install and create shortcut > Install.
On iOS and iPadOS there is no prompt: in Safari tap Share and then Add to Home
Screen (iOS 16.4 added the same entry to Chrome, Edge, Firefox, and Orion). Launch
the installed app: it opens without browser tabs, its first navigation carries
?source=pwa, and the page loads with the network disabled because the shell is
served from shell-v1.
See also
Section titled “See also”- Make it installable
- Offline strategies
- Manifest reference
- Service worker lifecycle
- Installation criteria (web.dev)
- Making PWAs installable (developer.mozilla.org)
← Back to the Guides overview.