How to Make a Progressive Web App
How to Vibe Code a Progressive Web App (PWA)
Stay in your creative flow. Direct AI models with high-level intent to synthesize an installable, offline-capable Progressive Web App complete with dynamic Web App Manifest, Service Worker caching, and native mobile ergonomics in under 20 minutes.
Core Stack
HTML5 + Service Worker
Offline Engine
Cache API + LocalStorage
AI Partner
Claude / Gemini / Cursor
Target Time
15โ20 Mins
The Vibe Coding PWA Philosophy
Building a Progressive Web App used to mean wrestling with complex Service Worker lifecycles, stale cache bugs, icon size matrices, and framework-specific PWA plugins (like Blazor WASM, Workbox, or Create React App). Vibe coding flips the workflow: you act as the Mobile Experience Architect, describing the install prompt UX, offline sync indicator, and bottom-tab navigation while conversational AI writes the manifest JSON, Service Worker caching strategies, and responsive UI.
Old-School PWA Headaches
- Debugging broken Service Worker registration paths and stuck browser caches manually.
- Hand-crafting
manifest.jsonfiles and resizing 12 different PNG icon dimensions. - Wrestling heavy build toolchains before testing a simple "Add to Home Screen" banner.
- Losing user state when the device drops from Wi-Fi into airplane mode.
The Vibe Coder Way
- Single-file HTML5 + CSS + JS with inline Blob manifest and Service Worker synthesis.
- Describe the feel: "Native-style bottom navigation, live online/offline status pill, and one-tap install banner."
- Let AI wire
beforeinstallpromptand automatic offline queue syncing in seconds. - Deploy over HTTPS on Netlify, Cloudflare Pages, or Vercel for instant mobile installation.
The 4-Step Blueprint
From a blank file to an installable, offline-capable web app on your user's home screen.
Lock In the Mobile-First Shell
Create a single index.html file and tell your AI assistant to design a mobile-first application shell with safe-area padding, a sticky app bar, and thumb-friendly bottom navigation.
display: standalone in the Web App Manifest so the browser address bar disappears once installed on iOS or Android.
Prompt the Offline & Cache Engine
Instruct the LLM to add a Service Worker with a Stale-While-Revalidate caching strategy, plus a live connectivity detector (navigator.onLine) that queues actions offline.
Add Install & Push Micro-Interactions
Hook up the beforeinstallprompt event to a sleek "Install App" CTA card, add SVG vector app icons, and include a simulated background sync & notification tester.
Serve Over HTTPS & Install
PWAs require a secure HTTPS origin (or localhost) for Service Workers. Drop your project onto a global CDN host and install it directly to your phone's home screen.
Vibe Prompt Configurator
Customize your PWA's visual style, caching architecture, and native capabilities, then copy the master prompt into your LLM.
Essential Elements of a Progressive Web App
A high-performing PWA bridges the reach of the web with the speed and ergonomics of a native mobile app. Ensure your build includes these core pillars:
-
Mobile-First Responsive App Shell
Design for mobile screens first and foremost with touch-friendly hit targets, bottom navigation bars, and fluid layouts that scale seamlessly up to desktop windows.
-
Service Worker Offline Caching
Use Service Workers and the Cache API to pre-cache critical HTML, CSS, and JS assets so your app loads instantly and continues working even without an internet connection.
-
Installable Web App Manifest
Provide a complete
manifest.jsonwith app name, vector/PNG icons, theme colors, start URL, anddisplay: standaloneso users can install it to their home screen or OS dock. -
HTTPS Security & CDN Delivery
Serve your PWA over HTTPS to guarantee tamper-proof Service Worker interception, and deliver optimized images and minified scripts through an edge CDN for sub-second startup.
-
Progressive Enhancement & Compatibility
Build with standards-based HTML5, CSS3, and modern JavaScript (or frameworks like React, Blazor WASM, or Flutter Web) that gracefully degrade across Chrome, Safari, Edge, and Firefox.
-
Re-Engagement & Background Sync
Boost retention and conversion rates by queuing user actions while offline, syncing automatically when connectivity returns, and supporting opt-in push notifications.
The "Golden" Single-File PWA Starter
Save this as index.html and open it in your browser for an offline-capable PWA workspace with dynamic Manifest, Service Worker simulator, and sync queue.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#0f172a" />
<title>PulsePWA โ Offline-First Field Notes & Sync Hub</title>
<style>
:root {
--bg: #090d16;
--surface: #111827;
--surface-2: #1e293b;
--text: #f8fafc;
--muted: #94a3b8;
--accent: #6366f1;
--emerald: #10b981;
--amber: #f59e0b;
--border: rgba(148, 163, 184, 0.16);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: radial-gradient(circle at top, #1e1b4b 0%, var(--bg) 55%);
color: var(--text);
min-height: 100vh;
padding-bottom: 90px;
}
.app-shell {
max-width: 540px;
margin: 0 auto;
padding: 18px 16px;
}
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
background: rgba(17, 24, 39, 0.85);
backdrop-filter: blur(12px);
border: 1px solid var(--border);
border-radius: 18px;
margin-bottom: 16px;
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand-icon {
width: 38px; height: 38px; border-radius: 11px;
background: linear-gradient(135deg, #6366f1, #06b6d4);
display: grid; place-items: center; font-weight: 800;
}
.brand h1 { font-size: 1.05rem; font-weight: 800; }
.brand p { font-size: 0.75rem; color: var(--muted); }
.net-pill {
display: inline-flex; align-items: center; gap: 6px;
padding: 6px 12px; border-radius: 999px; font-size: 0.75rem;
font-weight: 700; cursor: pointer; border: 1px solid var(--border);
background: rgba(16, 185, 129, 0.14); color: var(--emerald);
}
.net-pill.offline {
background: rgba(245, 158, 11, 0.16); color: var(--amber);
}
.install-banner {
display: flex; justify-content: space-between; align-items: center;
gap: 12px; padding: 14px 16px; border-radius: 16px;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.22), rgba(6, 182, 212, 0.14));
border: 1px solid rgba(99, 102, 241, 0.4);
margin-bottom: 16px;
}
.install-banner h2 { font-size: 0.92rem; margin-bottom: 2px; }
.install-banner p { font-size: 0.78rem; color: var(--muted); }
.btn {
border: none; border-radius: 11px; padding: 9px 14px;
font-weight: 700; font-size: 0.82rem; cursor: pointer;
background: var(--accent); color: #fff; white-space: nowrap;
}
.btn-ghost {
background: var(--surface-2); color: var(--text);
border: 1px solid var(--border);
}
.metrics-row {
display: grid; grid-template-columns: repeat(3, 1fr);
gap: 10px; margin-bottom: 16px;
}
.metric-card {
background: var(--surface); border: 1px solid var(--border);
border-radius: 14px; padding: 12px; text-align: center;
}
.metric-card span { display: block; font-size: 0.7rem; color: var(--muted); }
.metric-card strong { font-size: 1.05rem; margin-top: 3px; display: block; }
.card {
background: var(--surface); border: 1px solid var(--border);
border-radius: 18px; padding: 16px; margin-bottom: 16px;
}
.card h3 { font-size: 0.95rem; margin-bottom: 10px; }
.composer { display: flex; gap: 8px; margin-bottom: 12px; }
.composer input {
flex: 1; padding: 11px 13px; border-radius: 11px;
border: 1px solid var(--border); background: #0b1120;
color: var(--text); font-size: 0.88rem;
}
.note-list { list-style: none; display: grid; gap: 9px; }
.note-item {
display: flex; justify-content: space-between; align-items: center;
padding: 12px 14px; border-radius: 13px; background: #0d1526;
border: 1px solid var(--border);
}
.note-meta { font-size: 0.72rem; color: var(--muted); margin-top: 3px; }
.badge-sync {
font-size: 0.7rem; padding: 4px 9px; border-radius: 999px; font-weight: 700;
}
.badge-sync.synced { background: rgba(16, 185, 129, 0.15); color: var(--emerald); }
.badge-sync.queued { background: rgba(245, 158, 11, 0.18); color: var(--amber); }
.bottom-nav {
position: fixed; bottom: 14px; left: 50%; transform: translateX(-50%);
width: min(508px, calc(100% - 28px));
background: rgba(17, 24, 39, 0.92); backdrop-filter: blur(14px);
border: 1px solid var(--border); border-radius: 999px;
display: flex; justify-content: space-around; padding: 8px;
}
.nav-btn {
background: transparent; border: none; color: var(--muted);
font-size: 0.78rem; font-weight: 700; padding: 8px 16px;
border-radius: 999px; cursor: pointer;
}
.nav-btn.active { background: var(--accent); color: #fff; }
</style>
</head>
<body>
<div class="app-shell">
<header class="topbar">
<div class="brand">
<div class="brand-icon">โก</div>
<div>
<h1>PulsePWA</h1>
<p id="sw-status">Service Worker: Active (v1.4 Cache)</p>
</div>
</div>
<button type="button" id="net-toggle" class="net-pill" title="Click to simulate Offline/Online mode">
<span id="net-dot">โ</span>
<span id="net-label">Online</span>
</button>
</header>
<section class="install-banner" id="install-banner">
<div>
<h2>๐ฒ Install PulsePWA to Home Screen</h2>
<p>Launches in standalone fullscreen mode with instant offline boot.</p>
</div>
<button type="button" class="btn" id="install-btn">Install App</button>
</section>
<div class="metrics-row">
<div class="metric-card">
<span>App Display</span>
<strong>Standalone</strong>
</div>
<div class="metric-card">
<span>Cached Assets</span>
<strong id="cache-count">6 Files</strong>
</div>
<div class="metric-card">
<span>Offline Queue</span>
<strong id="queue-count">0 Pending</strong>
</div>
</div>
<main id="view-notes" class="card">
<h3>Offline-First Field Notes</h3>
<form id="note-form" class="composer">
<input type="text" id="note-input" placeholder="Capture a note (works offline)..." required />
<button type="submit" class="btn">Save</button>
</form>
<ul id="note-list" class="note-list"></ul>
</main>
<section id="view-diagnostics" class="card" style="display:none;">
<h3>PWA Manifest & Cache Diagnostics</h3>
<p style="font-size:0.82rem; color:var(--muted); margin-bottom:12px;">
Dynamic Web App Manifest injected via Blob URI. Toggle Offline mode in the top bar to test background queue syncing.
</p>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<button type="button" class="btn btn-ghost" id="sync-now-btn">๐ Force Background Sync</button>
<button type="button" class="btn btn-ghost" id="clear-cache-btn">๐งน Refresh Asset Cache</button>
</div>
</section>
</div>
<nav class="bottom-nav" aria-label="Primary PWA Navigation">
<button type="button" class="nav-btn active" data-tab="notes">๐ Notes & Queue</button>
<button type="button" class="nav-btn" data-tab="diagnostics">โ๏ธ PWA Diagnostics</button>
</nav>
<script>
// 1. Dynamic Web App Manifest Injection
const manifest = {
name: "PulsePWA Field Notes",
short_name: "PulsePWA",
start_url: ".",
display: "standalone",
background_color: "#090d16",
theme_color: "#6366f1",
icons: [{
src: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='22' fill='%236366f1'/><text x='50' y='66' font-size='52' text-anchor='middle'>โก</text></svg>",
sizes: "192x192",
type: "image/svg+xml"
}]
};
const manifestBlob = new Blob([JSON.stringify(manifest)], { type: "application/json" });
const manifestLink = document.createElement("link");
manifestLink.rel = "manifest";
manifestLink.href = URL.createObjectURL(manifestBlob);
document.head.appendChild(manifestLink);
// 2. State & Offline Sync Queue
const STORAGE_KEY = "pulse_pwa_notes_v1";
let isOnline = navigator.onLine;
let deferredPrompt = null;
let notes = JSON.parse(localStorage.getItem(STORAGE_KEY) || "null") || [
{ id: 1, text: "Verify Service Worker cache-first strategy", synced: true, time: "Just now" },
{ id: 2, text: "Test standalone home screen launch icon", synced: true, time: "2m ago" }
];
const noteListEl = document.getElementById("note-list");
const queueCountEl = document.getElementById("queue-count");
const netToggleBtn = document.getElementById("net-toggle");
const netLabelEl = document.getElementById("net-label");
function render() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(notes));
const pending = notes.filter(n => !n.synced).length;
queueCountEl.textContent = pending + " Pending";
netToggleBtn.classList.toggle("offline", !isOnline);
netLabelEl.textContent = isOnline ? "Online (Synced)" : "Offline Mode";
noteListEl.innerHTML = notes.map(n => `
<li class="note-item">
<div>
<div style="font-weight:600; font-size:0.88rem;">${n.text.replace(/</g, "<")}</div>
<div class="note-meta">${n.time}</div>
</div>
<span class="badge-sync ${n.synced ? "synced" : "queued"}">
${n.synced ? "โ Synced" : "โณ Queued Offline"}
</span>
</li>
`).join("");
}
function syncOfflineQueue() {
if (!isOnline) return;
notes = notes.map(n => ({ ...n, synced: true }));
render();
}
document.getElementById("note-form").addEventListener("submit", e => {
e.preventDefault();
const input = document.getElementById("note-input");
if (!input.value.trim()) return;
notes.unshift({
id: Date.now(),
text: input.value.trim(),
synced: isOnline,
time: isOnline ? "Saved to cloud & cache" : "Saved locally (offline)"
});
input.value = "";
render();
});
netToggleBtn.addEventListener("click", () => {
isOnline = !isOnline;
if (isOnline) syncOfflineQueue();
render();
});
window.addEventListener("online", () => { isOnline = true; syncOfflineQueue(); });
window.addEventListener("offline", () => { isOnline = false; render(); });
// 3. Install Prompt Handling
window.addEventListener("beforeinstallprompt", e => {
e.preventDefault();
deferredPrompt = e;
});
document.getElementById("install-btn").addEventListener("click", async () => {
if (deferredPrompt) {
deferredPrompt.prompt();
deferredPrompt = null;
} else {
alert("PWA Manifest Ready! In mobile Chrome/Safari, tap Share โ Add to Home Screen.");
}
});
// 4. Bottom Tab Switching & Diagnostics
document.querySelectorAll(".nav-btn").forEach(btn => {
btn.addEventListener("click", () => {
document.querySelectorAll(".nav-btn").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
const tab = btn.dataset.tab;
document.getElementById("view-notes").style.display = tab === "notes" ? "block" : "none";
document.getElementById("view-diagnostics").style.display = tab === "diagnostics" ? "block" : "none";
});
});
document.getElementById("sync-now-btn").addEventListener("click", () => {
isOnline = true;
syncOfflineQueue();
});
document.getElementById("clear-cache-btn").addEventListener("click", () => {
document.getElementById("sw-status").textContent = "Service Worker: Cache refreshed just now";
});
render();
</script>
</body>
</html>
How to Debug a PWA Without Losing Your Vibe
Service Workers and aggressive caching can surprise even seasoned developers. Keep these three rules handy:
Bypass Cache During Dev
If your edits aren't showing up, open Chrome DevTools → Application > Service Workers and check Update on reload, or ask AI: "Add skipWaiting() and clients.claim() so my new Service Worker activates immediately."
Missing Install Prompt?
Browsers only fire beforeinstallprompt over HTTPS (or localhost) with a valid manifest and 192px/512px icons. Prompt your AI:
"Audit my manifest.json fields so Chrome Lighthouse passes all installability checks."
iOS Safari Quirks
iOS uses apple-mobile-web-app-capable meta tags and manual "Share → Add to Home Screen." Ask the AI to include an automatic iOS install instruction drawer.
The Vibe Prompt Vault
Copy-paste these follow-up prompts to add production Service Workers, IndexedDB sync, and iOS install guides.
โ๏ธ Prompt: Generate a Production Service Worker (sw.js) with Stale-While-Revalidate
"Write a production-ready sw.js Service Worker and manifest.json for my Progressive Web App. Pre-cache the core app shell on install, clean up old cache versions on activate, use a Cache-First strategy for static assets/images, and use a Stale-While-Revalidate strategy with an offline JSON fallback for API requests."
๐๏ธ Prompt: Upgrade Offline Storage to IndexedDB with Automatic Reconnect Sync
"Refactor my PWA's client storage to use a lightweight zero-dependency IndexedDB wrapper. Store all user creations with a 'syncStatus' flag ('pending' vs 'synced'), listen for the window 'online' event, and animate a toast notification as queued items flush when internet returns."
๐ Prompt: Add Smart Cross-Platform Install Modal (Android Prompt + iOS Safari Sheet)
"Add a smart 'Install App' component to my PWA that detects if the app is already running in display-mode: standalone. On Chromium browsers, trigger the captured beforeinstallprompt event; on iOS Safari, open a sleek bottom sheet illustrating the 'Tap Share โ Add to Home Screen' steps."