Agent skill
react-pwa
Install this agent skill to your Project
npx add-skill https://github.com/jwynia/agent-skills/tree/main/skills/tech/frontend/pwa/react-pwa
SKILL.md
React PWA
Build Progressive Web Apps with React and Vite using vite-plugin-pwa. This skill covers the complete PWA lifecycle: manifest configuration, service worker strategies, offline support, installability, and push notifications.
When to Use This Skill
Use when:
- Creating a new PWA from scratch with React + Vite
- Converting an existing React app to a PWA
- Adding offline capabilities to a web application
- Implementing install prompts and app-like experiences
- Setting up push notifications
- Optimizing caching strategies for performance
Don't use when:
- Building server-rendered apps without client-side caching needs
- Working with Next.js (use next-pwa instead)
- Simple static sites without offline requirements
Prerequisites
- Node.js 18+ and npm/pnpm/yarn
- Vite 5+ with React template
- Deno runtime (for skill scripts)
- Source icon: 512x512 PNG or SVG for icon generation
Quick Start
1. Install Dependencies
npm install -D vite-plugin-pwa workbox-window
2. Configure Vite
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'mask-icon.svg'],
manifest: {
name: 'My React PWA',
short_name: 'ReactPWA',
description: 'A Progressive Web App built with React',
theme_color: '#ffffff',
background_color: '#ffffff',
display: 'standalone',
icons: [
{ src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },
{ src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' },
{ src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }
]
}
})
]
})
3. Generate Icons
deno run --allow-read --allow-write scripts/generate-icons.ts --input logo.png --output public/
4. Add Update Prompt (Optional)
// src/components/PWAUpdatePrompt.tsx
import { useRegisterSW } from 'virtual:pwa-register/react'
export function PWAUpdatePrompt() {
const {
needRefresh: [needRefresh, setNeedRefresh],
updateServiceWorker
} = useRegisterSW()
if (!needRefresh) return null
return (
<div className="pwa-toast">
<span>New content available!</span>
<button onClick={() => updateServiceWorker(true)}>Reload</button>
<button onClick={() => setNeedRefresh(false)}>Close</button>
</div>
)
}
Instructions
Phase 1: Project Setup
1a. Create New Vite React Project
npm create vite@latest my-pwa -- --template react-ts
cd my-pwa
npm install
npm install -D vite-plugin-pwa workbox-window
1b. Add to Existing Project
npm install -D vite-plugin-pwa workbox-window
Add TypeScript types for virtual modules:
// src/vite-env.d.ts (add to existing file)
/// <reference types="vite-plugin-pwa/client" />
Phase 2: Manifest Configuration
The web app manifest defines how the PWA appears when installed.
Required Fields
{
"name": "Full Application Name",
"short_name": "ShortName",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3b82f6",
"icons": []
}
Display Modes
| Mode | Description | Use Case |
|---|---|---|
fullscreen |
No browser UI, fills entire screen | Games, immersive experiences |
standalone |
App-like, with system UI only | Most PWAs |
minimal-ui |
Minimal browser controls | Apps needing navigation |
browser |
Standard browser tab | Fallback only |
Generate Manifest
deno run --allow-read --allow-write scripts/generate-manifest.ts \
--name "My App" \
--short-name "App" \
--theme "#3b82f6" \
--output public/manifest.webmanifest
Phase 3: Service Worker Strategies
vite-plugin-pwa uses Workbox under the hood. Choose a strategy based on your needs.
Strategy Options
| Strategy | Behavior | Best For |
|---|---|---|
generateSW |
Auto-generates SW from config | Most projects |
injectManifest |
Injects precache into custom SW | Custom caching logic |
Caching Strategies
CacheFirst - Serve from cache, fall back to network:
runtimeCaching: [
{
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts-cache',
expiration: { maxEntries: 10, maxAgeSeconds: 60 * 60 * 24 * 365 }
}
}
]
NetworkFirst - Try network, fall back to cache:
{
urlPattern: /^https:\/\/api\.example\.com\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: { maxEntries: 50, maxAgeSeconds: 60 * 60 * 24 }
}
}
StaleWhileRevalidate - Serve cache immediately, update in background:
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'images-cache',
expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 * 30 }
}
}
Complete Vite Config with Caching
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\..*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 }
}
},
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp)$/,
handler: 'CacheFirst',
options: {
cacheName: 'images',
expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 * 30 }
}
}
]
},
manifest: {
name: 'My React PWA',
short_name: 'ReactPWA',
theme_color: '#3b82f6',
background_color: '#ffffff',
display: 'standalone',
icons: [
{ src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },
{ src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' },
{ src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }
]
}
})
]
})
Phase 4: Offline Support
4a. Offline Fallback Page
Create an offline page that displays when network and cache both fail:
// vite.config.ts - add to VitePWA options
workbox: {
navigateFallback: '/offline.html',
navigateFallbackDenylist: [/^\/api/]
}
Create public/offline.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Offline</title>
<style>
body { font-family: system-ui; display: flex; align-items: center;
justify-content: center; min-height: 100vh; margin: 0; }
.offline { text-align: center; }
</style>
</head>
<body>
<div class="offline">
<h1>You're offline</h1>
<p>Please check your internet connection and try again.</p>
<button onclick="window.location.reload()">Retry</button>
</div>
</body>
</html>
4b. Offline Detection Hook
// src/hooks/useOnlineStatus.ts
import { useSyncExternalStore } from 'react'
function subscribe(callback: () => void) {
window.addEventListener('online', callback)
window.addEventListener('offline', callback)
return () => {
window.removeEventListener('online', callback)
window.removeEventListener('offline', callback)
}
}
export function useOnlineStatus() {
return useSyncExternalStore(
subscribe,
() => navigator.onLine,
() => true // SSR fallback
)
}
Usage:
function App() {
const isOnline = useOnlineStatus()
return (
<div>
{!isOnline && <Banner>You are offline. Some features may be limited.</Banner>}
{/* ... */}
</div>
)
}
Phase 5: Install Prompt
5a. Install Button Component
// src/components/InstallButton.tsx
import { useState, useEffect } from 'react'
interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<void>
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>
}
export function InstallButton() {
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null)
const [isInstalled, setIsInstalled] = useState(false)
useEffect(() => {
// Check if already installed
if (window.matchMedia('(display-mode: standalone)').matches) {
setIsInstalled(true)
return
}
const handler = (e: Event) => {
e.preventDefault()
setDeferredPrompt(e as BeforeInstallPromptEvent)
}
window.addEventListener('beforeinstallprompt', handler)
window.addEventListener('appinstalled', () => setIsInstalled(true))
return () => window.removeEventListener('beforeinstallprompt', handler)
}, [])
const handleInstall = async () => {
if (!deferredPrompt) return
deferredPrompt.prompt()
const { outcome } = await deferredPrompt.userChoice
if (outcome === 'accepted') {
setDeferredPrompt(null)
}
}
if (isInstalled || !deferredPrompt) return null
return (
<button onClick={handleInstall} className="install-btn">
Install App
</button>
)
}
Phase 6: Push Notifications
6a. Request Permission
async function requestNotificationPermission(): Promise<boolean> {
if (!('Notification' in window)) {
console.warn('Notifications not supported')
return false
}
if (Notification.permission === 'granted') return true
if (Notification.permission === 'denied') return false
const permission = await Notification.requestPermission()
return permission === 'granted'
}
6b. Subscribe to Push
async function subscribeToPush(vapidPublicKey: string): Promise<PushSubscription | null> {
const registration = await navigator.serviceWorker.ready
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
})
// Send subscription to your backend
await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
})
return subscription
}
function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = '='.repeat((4 - base64String.length % 4) % 4)
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
const rawData = window.atob(base64)
return Uint8Array.from([...rawData].map(char => char.charCodeAt(0)))
}
6c. Handle Push in Service Worker (injectManifest mode)
// src/sw.ts
import { precacheAndRoute } from 'workbox-precaching'
declare const self: ServiceWorkerGlobalScope
precacheAndRoute(self.__WB_MANIFEST)
self.addEventListener('push', (event) => {
const data = event.data?.json() ?? { title: 'Notification', body: 'New update available' }
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/pwa-192x192.png',
badge: '/badge-72x72.png'
})
)
})
self.addEventListener('notificationclick', (event) => {
event.notification.close()
event.waitUntil(
self.clients.openWindow('/')
)
})
Examples
Example 1: Basic PWA Setup
Scenario: Convert a Vite React app to an installable PWA with offline support.
Steps:
- Install dependencies:
npm install -D vite-plugin-pwa - Add VitePWA plugin to vite.config.ts
- Generate icons from source image
- Build and test:
npm run build && npm run preview - Verify in Chrome DevTools > Application > Manifest
Verification:
- Lighthouse PWA audit passes
- App is installable (install icon in address bar)
- Works offline after first load
Example 2: API-Heavy App with Smart Caching
Scenario: E-commerce app with product API calls that should work offline after browsing.
VitePWA({
workbox: {
runtimeCaching: [
// Cache product images aggressively
{
urlPattern: /\/products\/images\/.*/,
handler: 'CacheFirst',
options: {
cacheName: 'product-images',
expiration: { maxEntries: 200, maxAgeSeconds: 60 * 60 * 24 * 7 }
}
},
// Cache API responses with network-first
{
urlPattern: /\/api\/products\/.*/,
handler: 'NetworkFirst',
options: {
cacheName: 'product-api',
expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 },
networkTimeoutSeconds: 3
}
}
]
}
})
Example 3: Periodic Background Sync
Scenario: News app that syncs content in background.
// Register periodic sync
const registration = await navigator.serviceWorker.ready
await registration.periodicSync.register('sync-articles', {
minInterval: 60 * 60 * 1000 // 1 hour
})
// Handle in service worker
self.addEventListener('periodicsync', (event) => {
if (event.tag === 'sync-articles') {
event.waitUntil(syncArticles())
}
})
Script Reference
| Script | Purpose | Permissions |
|---|---|---|
generate-manifest.ts |
Create manifest.webmanifest | --allow-read --allow-write |
generate-icons.ts |
Generate icon set from source | --allow-read --allow-write --allow-run |
generate-sw-config.ts |
Create service worker config | --allow-read --allow-write |
audit-pwa.ts |
Validate PWA compliance | --allow-read --allow-net |
Generate Manifest
deno run --allow-read --allow-write scripts/generate-manifest.ts \
--name "My Application" \
--short-name "MyApp" \
--description "A great PWA" \
--theme "#3b82f6" \
--background "#ffffff" \
--display standalone \
--output public/manifest.webmanifest
Generate Icons
deno run --allow-read --allow-write --allow-run scripts/generate-icons.ts \
--input logo.svg \
--output public/ \
--sizes 192,512 \
--maskable
Audit PWA
deno run --allow-read --allow-net scripts/audit-pwa.ts \
--url http://localhost:5173 \
--format summary
Common Issues
Issue: Service Worker Not Updating
Symptoms: Old content served after deployment.
Solution:
- Ensure
registerType: 'autoUpdate'is set - Add update prompt component to notify users
- For immediate updates in development: Chrome DevTools > Application > Service Workers > Update on reload
Issue: App Not Installable
Symptoms: No install prompt, Lighthouse fails installability.
Solution:
- Verify manifest has all required fields (name, short_name, icons, start_url, display)
- Ensure icons are at least 192x192 and 512x512
- Serve over HTTPS (or localhost for development)
- Check for service worker registration errors in console
Issue: Caching API Responses Causes Stale Data
Symptoms: Users see outdated data.
Solution:
- Use
NetworkFirststrategy for dynamic API endpoints - Add
networkTimeoutSecondsfor faster fallback - Implement cache versioning with expiration
Issue: Push Notifications Not Working
Symptoms: Subscription succeeds but notifications don't arrive.
Solution:
- Verify VAPID keys match between frontend and backend
- Check service worker is active (not waiting)
- Ensure
userVisibleOnly: truein subscription - Test push delivery with web-push CLI tool
Additional Resources
Reference Files
references/caching-strategies.md- Deep dive into Workbox caching patternsreferences/push-notifications.md- Complete push notification backend setupreferences/testing-pwas.md- Testing strategies for PWA features
Assets
assets/manifest-template.json- Complete manifest with all optional fieldsassets/sw-template.ts- Custom service worker template for injectManifest
Limitations
- Push notifications require a backend server with web-push
- Background sync has limited browser support (Chrome/Edge)
- iOS Safari has PWA limitations (no push notifications, limited storage)
- Service worker debugging can be complex
Related Skills
- frontend-design - Design systems and component styling
- web-search - Research PWA best practices and browser support
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
document-to-narration
Convert written documents to narrated video scripts with TTS audio and word-level timing. Use when preparing essays, blog posts, or articles for video narration. Outputs scene files, audio, and VTT with precise word timestamps. Keywords: narration, voiceover, TTS, scenes, audio, timing, video script, spoken.
shadcn-layouts
Generate correct shadcn/Tailwind layouts by applying CSS mental models. This skill should be used when the user asks to 'create a shadcn layout', 'fix layout issues', 'debug CSS height problems', 'make scrolling work', or has issues with Tailwind flex/grid. Keywords: shadcn, Tailwind, layout, CSS, flex, grid, height, scroll, overflow.
frontend-design
Create distinctive, production-grade frontend interfaces with high design quality. Provides analysis tools for auditing existing designs and generation tools for creating color palettes, typography systems, design tokens, and component templates. Supports React, Vue, Svelte, and vanilla HTML/CSS. Use when building web components, pages, or applications. Keywords: design, UI, frontend, CSS, components, palette, typography, tokens, accessibility.
pwa-development
Implement Progressive Web App features for React and Svelte projects. This skill should be used when the user asks to 'make a PWA', 'add offline support', 'create a service worker', 'fix caching issues', or wants installable web apps. Keywords: PWA, service worker, offline, manifest, caching, installable, Workbox, vite-pwa.
mastra-hono
Develop AI agents, tools, and workflows with Mastra v1 Beta and Hono servers. This skill should be used when creating Mastra agents, defining tools with Zod schemas, building workflows with step data flow, setting up Hono API servers with Mastra adapters, or implementing agent networks. Keywords: mastra, hono, agent, tool, workflow, AI, LLM, typescript, API, MCP.
abstract-strategy
Design abstract strategy games with perfect information, no randomness, and strategic depth. Use when designing a board game, exploring abstract strategy games, brainstorming game mechanics, or evaluating game balance. Keywords: board game, game design, strategy, mechanics, balance.
Didn't find tool you were looking for?