
Progressive Web App
- 452 installs
- 305 repo stars
- Updated March 4, 2026
- aj-geddes/useful-ai-prompts
progressive-web-app is a useful-ai-prompts skill that guides installable PWA implementation with service workers, manifests, offline caching, push notifications, and responsive app-shell UX for developers shipping browse
About
progressive-web-app is a skill from aj-geddes/useful-ai-prompts that walks developers through building installable Progressive Web Apps. It covers web app manifests, service worker registration, offline caching strategies, push notification setup, and responsive app-shell UX patterns across browsers. Developers reach for progressive-web-app when a web product should behave like an installable app with offline resilience and mobile-friendly shells, and they need structured guidance instead of piecing together manifest, worker, and caching steps from scattered documentation.
- Service worker lifecycle
- Web app manifest
- Offline caching strategies
- Install prompts and UX
- Performance and resilience
Progressive Web App by the numbers
- 452 all-time installs (skills.sh)
- Ranked #637 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aj-geddes/useful-ai-prompts --skill progressive-web-appAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 452 |
|---|---|
| repo stars | ★ 305 |
| Last updated | March 4, 2026 |
| Repository | aj-geddes/useful-ai-prompts ↗ |
How do you build an installable progressive web app?
Guide implementation of installable PWAs with service workers, manifests, offline caching, push notifications, and responsive app-shell UX across browsers.
Who is it for?
Frontend developers converting a responsive web app into an installable, offline-capable PWA across major browsers.
Skip if: Developers building native iOS or Android store apps only, because progressive-web-app focuses on browser-based PWA standards.
When should I use this skill?
The user asks to add a web manifest, service worker, offline caching, push notifications, or app-shell UX to a web application.
What you get
Web app manifest, service worker, offline caching configuration, push notification setup, and responsive app-shell UX.
- Web app manifest
- Service worker file
- Offline caching configuration
Files
Progressive Web App
Table of Contents
Overview
Build progressive web applications with offline support, installability, service workers, and web app manifests to deliver app-like experiences in the browser.
When to Use
- App-like web experiences
- Offline functionality needed
- Mobile installation required
- Push notifications
- Fast loading experiences
Quick Start
Minimal working example:
// public/manifest.json
{
"name": "My Awesome App",
"short_name": "AwesomeApp",
"description": "A progressive web application",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait-primary",
"background_color": "#ffffff",
"theme_color": "#007bff",
"icons": [
{
"src": "/images/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/images/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
// ... (see reference guides for full implementation)Reference Guides
Detailed implementations in the references/ directory:
| Guide | Contents |
|---|---|
| Web App Manifest | Web App Manifest |
| Service Worker Implementation | Service Worker Implementation |
| Install Prompt and App Installation | Install Prompt and App Installation |
| Offline Support with IndexedDB | Offline Support with IndexedDB |
| Push Notifications | Push Notifications |
Best Practices
✅ DO
- Follow established patterns and conventions
- Write clean, maintainable code
- Add appropriate documentation
- Test thoroughly before deploying
❌ DON'T
- Skip testing or validation
- Ignore error handling
- Hard-code configuration values
Install Prompt and App Installation
Install Prompt and App Installation
// hooks/useInstallPrompt.ts
import { useState, useEffect } from 'react';
interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<void>;
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}
export const useInstallPrompt = () => {
const [promptEvent, setPromptEvent] = useState<BeforeInstallPromptEvent | null>(null);
const [isInstalled, setIsInstalled] = useState(false);
const [isIOSInstalled, setIsIOSInstalled] = useState(false);
useEffect(() => {
const handleBeforeInstallPrompt = (e: Event) => {
e.preventDefault();
setPromptEvent(e as BeforeInstallPromptEvent);
};
const handleAppInstalled = () => {
setIsInstalled(true);
setPromptEvent(null);
};
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
window.addEventListener('appinstalled', handleAppInstalled);
// Check if running as installed app
if (window.matchMedia('(display-mode: standalone)').matches) {
setIsInstalled(true);
}
// Check iOS
const isIOSDevice = /iPad|iPhone|iPod/.test(navigator.userAgent);
const isIOSApp = navigator.standalone === true;
if (isIOSDevice && !isIOSApp) {
setIsIOSInstalled(false);
}
return () => {
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
window.removeEventListener('appinstalled', handleAppInstalled);
};
}, []);
const installApp = async () => {
if (promptEvent) {
await promptEvent.prompt();
const { outcome } = await promptEvent.userChoice;
if (outcome === 'accepted') {
setIsInstalled(true);
}
setPromptEvent(null);
}
};
return {
promptEvent,
canInstall: promptEvent !== null,
isInstalled,
isIOSInstalled,
installApp
};
};
// components/InstallPrompt.tsx
export const InstallPrompt: React.FC = () => {
const { canInstall, isInstalled, installApp } = useInstallPrompt();
if (isInstalled || !canInstall) return null;
return (
<div className="install-prompt">
<h2>Install App</h2>
<p>Install our app for quick access and offline support</p>
<button onClick={installApp}>Install</button>
</div>
);
};Offline Support with IndexedDB
Offline Support with IndexedDB
// db/notesDB.ts
import { openDB, DBSchema, IDBPDatabase } from "idb";
interface Note {
id: string;
title: string;
content: string;
timestamp: number;
synced: boolean;
}
interface NotesDB extends DBSchema {
notes: {
key: string;
value: Note;
indexes: { "by-timestamp": number; "by-synced": boolean };
};
}
let db: IDBPDatabase<NotesDB>;
export async function initDB() {
db = await openDB<NotesDB>("notes-db", 1, {
upgrade(db) {
const store = db.createObjectStore("notes", { keyPath: "id" });
store.createIndex("by-timestamp", "timestamp");
store.createIndex("by-synced", "synced");
},
});
return db;
}
export async function addNote(note: Omit<Note, "timestamp">) {
return db.add("notes", {
...note,
timestamp: Date.now(),
synced: false,
});
}
export async function getNotes(): Promise<Note[]> {
return db.getAll("notes");
}
export async function getUnsyncedNotes(): Promise<Note[]> {
return db.getAllFromIndex("notes", "by-synced", false);
}
export async function updateNote(id: string, updates: Partial<Note>) {
const note = await db.get("notes", id);
if (note) {
await db.put("notes", { ...note, ...updates });
}
}
export async function markAsSynced(id: string) {
await updateNote(id, { synced: true });
}Push Notifications
Push Notifications
// services/pushNotification.ts
export async function subscribeToPushNotifications() {
if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
console.log("Push notifications not supported");
return;
}
try {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: process.env.REACT_APP_VAPID_PUBLIC_KEY,
});
// Send subscription to server
await fetch("/api/push-subscription", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(subscription),
});
return subscription;
} catch (error) {
console.error("Push subscription failed:", error);
}
}
// service-worker.ts
self.addEventListener("push", (event: PushEvent) => {
const data = event.data?.json() ?? {};
const options: NotificationOptions = {
title: data.title || "New Notification",
body: data.message || "",
icon: "/images/icon-192.png",
badge: "/images/badge-72.png",
tag: data.tag || "notification",
};
event.waitUntil(self.registration.showNotification(options.title, options));
});
self.addEventListener("notificationclick", (event: NotificationEvent) => {
event.notification.close();
event.waitUntil(
self.clients.matchAll({ type: "window" }).then((clients) => {
if (clients.length > 0) {
return clients[0].focus();
}
return self.clients.openWindow("/");
}),
);
});Service Worker Implementation
Service Worker Implementation
// public/service-worker.ts
const CACHE_NAME = "app-v1";
const STATIC_ASSETS = [
"/",
"/index.html",
"/css/main.css",
"/js/app.js",
"/images/icon-192.png",
"/offline.html",
];
// Install event
self.addEventListener("install", (event: ExtendableEvent) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(STATIC_ASSETS);
}),
);
self.skipWaiting();
});
// Activate event
self.addEventListener("activate", (event: ExtendableEvent) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name)),
);
}),
);
self.clients.claim();
});
// Fetch event with cache-first strategy for static assets
self.addEventListener("fetch", (event: FetchEvent) => {
const { request } = event;
// Skip non-GET requests
if (request.method !== "GET") {
return;
}
// Cache first for static assets
if (request.destination === "image" || request.destination === "font") {
event.respondWith(
caches
.match(request)
.then((response) => {
return (
response ||
fetch(request).then((res) => {
if (res.ok) {
const clone = res.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, clone);
});
}
return res;
})
);
})
.catch(() => {
return caches.match("/offline.html");
}),
);
}
// Network first for API calls
if (request.url.includes("/api/")) {
event.respondWith(
fetch(request)
.then((response) => {
if (response.ok) {
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, clone);
});
}
return response;
})
.catch(() => {
return caches.match(request);
}),
);
}
// Stale while revalidate for HTML
if (request.destination === "document") {
event.respondWith(
caches.match(request).then((cachedResponse) => {
const fetchPromise = fetch(request).then((response) => {
if (response.ok) {
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, response.clone());
});
}
return response;
});
return cachedResponse || fetchPromise;
}),
);
}
});
// Background Sync
self.addEventListener("sync", (event: any) => {
if (event.tag === "sync-notes") {
event.waitUntil(syncNotes());
}
});
async function syncNotes() {
const db = await openDB("notes");
const unsynced = await db.getAll(
"keyval",
IDBKeyRange.bound("pending_", "pending_\uffff"),
);
for (const item of unsynced) {
try {
await fetch("/api/notes", {
method: "POST",
body: JSON.stringify(item.value),
});
await db.delete("keyval", item.key);
} catch (error) {
console.error("Sync failed:", error);
}
}
}Web App Manifest
Web App Manifest
// public/manifest.json
{
"name": "My Awesome App",
"short_name": "AwesomeApp",
"description": "A progressive web application",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait-primary",
"background_color": "#ffffff",
"theme_color": "#007bff",
"icons": [
{
"src": "/images/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/images/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/images/icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/images/icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"screenshots": [
{
"src": "/images/screenshot-1.png",
"sizes": "540x720",
"type": "image/png",
"form_factor": "narrow"
},
{
"src": "/images/screenshot-2.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
}
],
"categories": ["productivity", "utilities"],
"shortcuts": [
{
"name": "Quick Note",
"short_name": "Note",
"description": "Create a quick note",
"url": "/new-note",
"icons": [
{
"src": "/images/note-icon.png",
"sizes": "192x192"
}
]
}
]
}<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#007bff" />
<link rel="manifest" href="/manifest.json" />
<link rel="icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/images/icon-192.png" />
<title>My Awesome App</title>
</head>
<body>
<div id="root"></div>
</body>
</html>// Component: [Name]
// TODO: Customize for your framework (React, Vue, Svelte, etc.)
import React from 'react';
interface Props {
// TODO: Define props
}
export function ComponentName({ }: Props) {
// TODO: Add state and effects
return (
<div>
{/* TODO: Add component markup */}
</div>
);
}
Related skills
How it compares
Pick progressive-web-app over generic frontend checklists when installability, service workers, offline caching, and push need end-to-end PWA guidance.
FAQ
What PWA features does progressive-web-app cover?
progressive-web-app covers web app manifests, service workers, offline caching, push notifications, and responsive app-shell UX. The skill guides implementation so the site becomes installable and resilient across browsers.
Is progressive-web-app for native mobile apps?
No. progressive-web-app targets browser-based Progressive Web Apps using standard web APIs. It is for developers who want installable, offline-capable web experiences rather than separate native store binaries.