pwa
Progressive Web App (PWA) Skill
Build installable, offline-capable web apps optimized for mobile with desktop compatibility.
Essential HTML Head
<head>
<!-- Viewport with safe area support -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="theme-color" content="#000000">
<!-- PWA capable -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="App Name">
<!-- Manifest & Icons -->
<link rel="manifest" href="/manifest.webmanifest">
<link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png">
</head>
Web App Manifest
{
"name": "My Progressive Web App",
"short_name": "MyPWA",
"description": "App description",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "any",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/icon-512-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}
Display Modes
| Mode | Description | Use Case |
|---|---|---|
standalone |
Native app look, no browser UI | Most apps (recommended) |
fullscreen |
Entire screen, no status bar | Games, immersive, VR/AR |
minimal-ui |
Minimal browser controls | Content needing navigation |
browser |
Standard browser tab | Not recommended for PWAs |
Detect Display Mode
@media (display-mode: standalone) {
.browser-nav { display: none; }
}
const isInstalled = window.matchMedia('(display-mode: standalone)').matches
|| window.navigator.standalone; // iOS
Safe Area Handling
Required: viewport-fit=cover in viewport meta tag.
Handles notches, Dynamic Island, rounded corners on modern devices.
:root {
--safe-top: env(safe-area-inset-top, 0px);
--safe-right: env(safe-area-inset-right, 0px);
--safe-bottom: env(safe-area-inset-bottom, 0px);
--safe-left: env(safe-area-inset-left, 0px);
}
body {
padding: var(--safe-top) var(--safe-right) var(--safe-bottom) var(--safe-left);
}
/* Fixed header */
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
padding: calc(1rem + var(--safe-top)) calc(1rem + var(--safe-right)) 1rem calc(1rem + var(--safe-left));
}
/* Fixed bottom navigation */
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 0.5rem var(--safe-right) calc(0.5rem + var(--safe-bottom)) var(--safe-left);
}
/* Landscape notch handling */
@media (orientation: landscape) {
.content {
padding-left: max(1rem, var(--safe-left));
padding-right: max(1rem, var(--safe-right));
}
}
iOS Status Bar Styles
| Value | Effect |
|---|---|
default |
White bar, black text |
black |
Black bar, white text |
black-translucent |
Transparent, content flows behind |
Service Worker
Registration
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW registered:', reg.scope))
.catch(err => console.error('SW failed:', err));
}
Basic Service Worker (sw.js)
const CACHE_NAME = 'app-v1';
const ASSETS = ['/', '/index.html', '/styles.css', '/app.js'];
// Install: cache assets
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(ASSETS))
.then(() => self.skipWaiting())
);
});
// Activate: clean old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
).then(() => self.clients.claim())
);
});
// Fetch: cache-first for assets, network-first for API
self.addEventListener('fetch', event => {
const { request } = event;
if (request.url.includes('/api/')) {
// Network first for API
event.respondWith(
fetch(request)
.then(res => {
const clone = res.clone();
caches.open(CACHE_NAME).then(c => c.put(request, clone));
return res;
})
.catch(() => caches.match(request))
);
} else {
// Cache first for static assets
event.respondWith(
caches.match(request).then(cached => cached || fetch(request))
);
}
});
Caching Strategies
| Strategy | Use Case | Behavior |
|---|---|---|
| Cache First | Static assets, fonts, images | Fast, may be stale |
| Network First | API data, dynamic content | Fresh, slower |
| Stale While Revalidate | Semi-dynamic content | Fast + background update |
| Network Only | Auth, real-time data | Always fresh |
Mobile Optimization
Touch Targets
/* Apple HIG: minimum 44x44px */
button, a, [role="button"] {
min-width: 44px;
min-height: 44px;
}
Prevent iOS Input Zoom
/* Font size >= 16px prevents zoom on focus */
input, select, textarea {
font-size: 16px;
}
Disable Pull-to-Refresh
html {
overscroll-behavior-y: contain;
}
Native-like Touch Feedback
button, a {
-webkit-tap-highlight-color: transparent;
touch-action: manipulation; /* Disable double-tap zoom */
}
/* Disable text selection on UI elements */
.nav, .toolbar {
-webkit-user-select: none;
user-select: none;
}
Smooth Scrolling
.scroll-container {
overflow-y: auto;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
}
Responsive Layout
/* Mobile-first */
.container {
padding: 1rem;
max-width: 100%;
}
/* Tablet */
@media (min-width: 768px) {
.container { max-width: 720px; margin: 0 auto; }
.mobile-only { display: none; }
}
/* Desktop */
@media (min-width: 1024px) {
.container { max-width: 960px; }
.bottom-nav { display: none; }
.sidebar { display: block; }
}
Installation Prompt
let deferredPrompt;
window.addEventListener('beforeinstallprompt', e => {
e.preventDefault();
deferredPrompt = e;
showInstallButton();
});
function installApp() {
if (!deferredPrompt) return;
deferredPrompt.prompt();
deferredPrompt.userChoice.then(result => {
console.log('Install:', result.outcome);
deferredPrompt = null;
});
}
window.addEventListener('appinstalled', () => {
console.log('App installed');
hideInstallButton();
});
PWA Checklist
Required for Installation
- HTTPS (localhost allowed for dev)
- Valid manifest with
name,icons,start_url,display - 192x192 PNG icon
- 512x512 PNG icon
- Service worker with fetch handler
Recommended
-
viewport-fit=covermeta tag - Safe area inset handling
-
theme_colorin manifest and meta tag - Maskable icon (512x512 with 20% safe zone)
- Apple touch icon (180x180)
-
apple-mobile-web-app-status-bar-stylemeta tag - Offline fallback page
- Install prompt UI
Performance
- Precache critical assets
- Lazy load non-critical resources
- Use WebP/AVIF images
- Code splitting
Testing
Lighthouse
Chrome DevTools > Lighthouse > Progressive Web App
Manual Checks
// Is installed?
window.matchMedia('(display-mode: standalone)').matches
// Service worker status
navigator.serviceWorker.getRegistrations()
.then(regs => console.log('SW:', regs));
// Cache contents
caches.keys().then(names => console.log('Caches:', names));
Clear PWA State
// Unregister all service workers
navigator.serviceWorker.getRegistrations()
.then(regs => regs.forEach(r => r.unregister()));
// Clear all caches
caches.keys().then(names => names.forEach(n => caches.delete(n)));
Reference Files
- reference/build-tools.md - Vite, Webpack, framework-specific setup
- reference/caching-strategies.md - Advanced Workbox patterns
- reference/mobile-optimization.md - iOS quirks, responsive patterns
More from sebastiaanwouters/dotagents
teacher
Guide learning and deep understanding through proven methodologies (Socratic, Feynman, Problem-Based). Use when user says "help me understand", "teach me", "explain this", "learn about", "socratic", "feynman", "problem-based", "I don't understand", "confused about", "why does", or wants to truly grasp a concept.
77chef
Telegram communication for AI agents. ALL methods are BLOCKING. Use for user interviews, status updates, and feedback collection.
34bitwarden
Retrieves API keys, passwords, secrets from Bitwarden vault using bw CLI. Triggers on missing env variables, missing API keys, missing secrets, "secret not found", "env not set", or "use bw".
29librarian
Use for code research that needs dependency internals, upstream implementation examples, or external prior art. Always delegate to a subagent that investigates with opensrc and web search, then return only distilled findings, versions, paths, and links.
29frontend-design
Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications. Generates creative, polished code that avoids generic AI aesthetics.
28ios-simulator-pwa-testing
Tests and debugs PWA apps using iOS Simulator on macOS. Use for testing PWA look/feel, debugging Safari console logs, taking screenshots, accessibility audits, and service worker debugging without a physical iOS device.
20