Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    jwynia

    react-pwa

    jwynia/react-pwa
    Coding
    13
    4 installs

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

    Build Progressive Web Apps with React and Vite...

    SKILL.md


    name: react-pwa description: Build Progressive Web Apps with React and Vite. This skill should be used when the user asks to "create a PWA", "add offline support", "make app installable", "generate service worker", "configure workbox", "add push notifications", or mentions PWA, progressive web app, or offline-first development. Keywords: PWA, progressive web app, service worker, manifest, offline, installable, workbox, vite-plugin-pwa, React. license: MIT compatibility: Requires Deno for scripts. Works with React 18+ and Vite 5+. metadata: author: agent-skills version: "1.0" type: utility mode: assistive domain: development

    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:

    1. Install dependencies: npm install -D vite-plugin-pwa
    2. Add VitePWA plugin to vite.config.ts
    3. Generate icons from source image
    4. Build and test: npm run build && npm run preview
    5. 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:

    1. Ensure registerType: 'autoUpdate' is set
    2. Add update prompt component to notify users
    3. 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:

    1. Verify manifest has all required fields (name, short_name, icons, start_url, display)
    2. Ensure icons are at least 192x192 and 512x512
    3. Serve over HTTPS (or localhost for development)
    4. Check for service worker registration errors in console

    Issue: Caching API Responses Causes Stale Data

    Symptoms: Users see outdated data.

    Solution:

    1. Use NetworkFirst strategy for dynamic API endpoints
    2. Add networkTimeoutSeconds for faster fallback
    3. Implement cache versioning with expiration

    Issue: Push Notifications Not Working

    Symptoms: Subscription succeeds but notifications don't arrive.

    Solution:

    1. Verify VAPID keys match between frontend and backend
    2. Check service worker is active (not waiting)
    3. Ensure userVisibleOnly: true in subscription
    4. Test push delivery with web-push CLI tool

    Additional Resources

    Reference Files

    • references/caching-strategies.md - Deep dive into Workbox caching patterns
    • references/push-notifications.md - Complete push notification backend setup
    • references/testing-pwas.md - Testing strategies for PWA features

    Assets

    • assets/manifest-template.json - Complete manifest with all optional fields
    • assets/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 Servers
    InstantDB
    InstantDB
    Svelte
    Svelte
    Vercel
    Vercel
    Repository
    jwynia/agent-skills
    Files