Progressive Web App (PWA) APIs

1. Web App Manifest API for App Installation

Manifest Property Description Example
name Full name of the app displayed during installation. "My Awesome PWA"
short_name Short name for home screen (limit 12 chars). "MyPWA"
start_url URL to open when app is launched. "/app?source=pwa"
display Display mode: "standalone", "fullscreen", "minimal-ui", "browser". "standalone"
background_color Background color during splash screen. "#ffffff"
theme_color Theme color for browser UI. "#4285f4"
icons Array of icon objects with src, sizes, type, purpose. [{src: "/icon-192.png", sizes: "192x192"}]
orientation Preferred orientation: "portrait", "landscape", "any". "portrait"
scope Navigation scope - URLs outside this scope open in browser. "/app/"
description Description of the app. "A powerful task manager"

Example: Complete Web App Manifest

{
  "name": "Task Manager Pro",
  "short_name": "TaskPro",
  "description": "Professional task management for teams",
  "start_url": "/app?source=pwa",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#2196f3",
  "orientation": "portrait-primary",
  "scope": "/app/",
  "icons": [
    {
      "src": "/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/maskable-icon.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "maskable"
    }
  ],
  "screenshots": [
    {
      "src": "/screenshots/desktop-1.png",
      "sizes": "1280x720",
      "type": "image/png",
      "form_factor": "wide"
    },
    {
      "src": "/screenshots/mobile-1.png",
      "sizes": "750x1334",
      "type": "image/png",
      "form_factor": "narrow"
    }
  ],
  "categories": ["productivity", "business"],
  "lang": "en-US",
  "dir": "ltr"
}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Task Manager Pro</title>
  
  <!-- Link to Web App Manifest -->
  <link rel="manifest" href="/manifest.json">
  
  <!-- Theme color for browser chrome -->
  <meta name="theme-color" content="#2196f3">
  
  <!-- Apple-specific tags -->
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="default">
  <meta name="apple-mobile-web-app-title" content="TaskPro">
  <link rel="apple-touch-icon" href="/icons/icon-192x192.png">
</head>
<body>
  <h1>Task Manager Pro</h1>
  <script>
    // Check if manifest is linked
    if ('manifest' in document.createElement('link')) {
      console.log("Web App Manifest supported");
    }
    
    // Get manifest programmatically (limited support)
    if ('getManifest' in document) {
      document.getManifest().then(manifest => {
        console.log("Manifest:", manifest);
      });
    }
  </script>
</body>
</html>
Note: Web App Manifest is a JSON file defining PWA metadata. Link with <link rel="manifest">. Icons should include multiple sizes (72px to 512px). Use "purpose": "maskable" for adaptive icons. Screenshots shown in install prompts on some platforms.

2. App Installation Prompts and beforeinstallprompt

Event/Method Description
beforeinstallprompt Event fired when browser is ready to show install prompt.
event.prompt() Shows the install prompt (must be called from user gesture).
event.userChoice Promise resolving to user's choice: "accepted" or "dismissed".
appinstalled Event fired when app is successfully installed.
Install Criteria (Chrome/Edge) Required
HTTPS ✓ Site must be served over HTTPS (or localhost)
Web App Manifest ✓ Valid manifest with name, icons, start_url, display
Service Worker ✓ Registered service worker with fetch event handler
User Engagement ✓ User has visited site at least once

Example: Custom install button with beforeinstallprompt

// Store the install prompt event
let deferredPrompt;
const installButton = document.getElementById("install-button");

// Hide install button initially
installButton.style.display = "none";

// Listen for beforeinstallprompt event
window.addEventListener("beforeinstallprompt", (event) => {
  console.log("beforeinstallprompt fired");
  
  // Prevent the default browser install prompt
  event.preventDefault();
  
  // Store the event for later use
  deferredPrompt = event;
  
  // Show custom install button
  installButton.style.display = "block";
  
  // Track that prompt is available
  analytics.track("install_prompt_available");
});

// Handle custom install button click
installButton.addEventListener("click", async () => {
  if (!deferredPrompt) {
    console.log("Install prompt not available");
    return;
  }
  
  // Hide the install button
  installButton.style.display = "none";
  
  // Show the install prompt
  deferredPrompt.prompt();
  
  // Wait for user's choice
  const choiceResult = await deferredPrompt.userChoice;
  
  console.log(`User choice: ${choiceResult.outcome}`);
  
  if (choiceResult.outcome === "accepted") {
    console.log("User accepted the install prompt");
    analytics.track("install_accepted");
  } else {
    console.log("User dismissed the install prompt");
    analytics.track("install_dismissed");
    
    // Show button again after dismissal (optional)
    // installButton.style.display = "block";
  }
  
  // Clear the deferred prompt
  deferredPrompt = null;
});

// Listen for successful installation
window.addEventListener("appinstalled", (event) => {
  console.log("PWA installed successfully");
  
  // Hide install button
  installButton.style.display = "none";
  
  // Clear deferred prompt
  deferredPrompt = null;
  
  // Track installation
  analytics.track("app_installed");
  
  // Show thank you message
  showNotification("Thanks for installing our app!");
});

Example: Check if already installed and detection

// Check if app is already installed (display mode detection)
function isInstalled() {
  // Check if running in standalone mode
  if (window.matchMedia("(display-mode: standalone)").matches) {
    return true;
  }
  
  // Check Safari-specific property
  if (window.navigator.standalone === true) {
    return true;
  }
  
  return false;
}

// Use on page load
if (isInstalled()) {
  console.log("App is installed and running in standalone mode");
  
  // Hide install button permanently
  document.getElementById("install-button").style.display = "none";
  
  // Show app-specific features
  showInstalledUserFeatures();
} else {
  console.log("App is running in browser");
  
  // Show install promotion
  showInstallPromotion();
}

// Listen for display mode changes
const displayModeMediaQuery = window.matchMedia("(display-mode: standalone)");

displayModeMediaQuery.addEventListener("change", (event) => {
  if (event.matches) {
    console.log("Switched to standalone mode");
  } else {
    console.log("Switched to browser mode");
  }
});

// Smart install prompt timing
class InstallPromptManager {
  constructor() {
    this.promptShown = localStorage.getItem("install_prompt_shown") === "true";
    this.installDismissed = localStorage.getItem("install_dismissed") === "true";
    this.visitCount = parseInt(localStorage.getItem("visit_count") || "0");
  }
  
  incrementVisit() {
    this.visitCount++;
    localStorage.setItem("visit_count", this.visitCount.toString());
  }
  
  shouldShowPrompt() {
    // Don't show if already shown or dismissed
    if (this.promptShown || this.installDismissed) {
      return false;
    }
    
    // Show after 3 visits
    if (this.visitCount < 3) {
      return false;
    }
    
    // Add more custom logic (e.g., time on site, engagement)
    return true;
  }
  
  markPromptShown() {
    this.promptShown = true;
    localStorage.setItem("install_prompt_shown", "true");
  }
  
  markDismissed() {
    this.installDismissed = true;
    localStorage.setItem("install_dismissed", "true");
  }
}

// Usage
const promptManager = new InstallPromptManager();
promptManager.incrementVisit();

window.addEventListener("beforeinstallprompt", (event) => {
  event.preventDefault();
  deferredPrompt = event;
  
  if (promptManager.shouldShowPrompt()) {
    // Show install UI
    showInstallUI();
    promptManager.markPromptShown();
  }
});
Note: beforeinstallprompt allows custom install UX. Call event.preventDefault() to prevent default prompt. Store event and call prompt() from user gesture. Check userChoice promise for outcome. Not available on iOS Safari - use custom install instructions instead.
Warning: beforeinstallprompt only available on Chrome, Edge, and Android browsers. Not supported on iOS/Safari. Install criteria vary by browser. Prompt() can only be called once per event. Must be called from user gesture (click, touch).

3. Window Controls Overlay for Desktop PWAs

Feature Description
Window Controls Overlay Allows PWA to use title bar area for custom content on desktop.
navigator.windowControlsOverlay API to interact with window controls overlay.
getTitlebarAreaRect() Returns DOMRect of title bar area.
visible Boolean indicating if overlay is visible.
geometrychange Event fired when title bar geometry changes.

Example: Enable and use Window Controls Overlay

{
  "name": "Desktop PWA",
  "display": "standalone",
  "display_override": ["window-controls-overlay"],
  "theme_color": "#2196f3"
}

Example: Handle title bar area in JavaScript

// Check if Window Controls Overlay is available
if ("windowControlsOverlay" in navigator) {
  const windowControls = navigator.windowControlsOverlay;
  
  console.log("Overlay visible:", windowControls.visible);
  
  if (windowControls.visible) {
    // Get title bar area dimensions
    const titleBarRect = windowControls.getTitlebarAreaRect();
    
    console.log("Title bar area:", {
      x: titleBarRect.x,
      y: titleBarRect.y,
      width: titleBarRect.width,
      height: titleBarRect.height
    });
    
    // Update custom title bar layout
    updateTitleBarLayout(titleBarRect);
  }
  
  // Listen for geometry changes (resize, maximize, etc.)
  windowControls.addEventListener("geometrychange", (event) => {
    console.log("Title bar geometry changed");
    
    const titleBarRect = windowControls.getTitlebarAreaRect();
    const isVisible = windowControls.visible;
    
    console.log("New dimensions:", titleBarRect);
    console.log("Visible:", isVisible);
    
    // Update layout
    updateTitleBarLayout(titleBarRect);
  });
}

// Update title bar layout
function updateTitleBarLayout(rect) {
  const titleBar = document.getElementById("custom-title-bar");
  
  if (titleBar) {
    // Position title bar content avoiding system controls
    titleBar.style.position = "fixed";
    titleBar.style.top = `${rect.y}px`;
    titleBar.style.left = `${rect.x}px`;
    titleBar.style.width = `${rect.width}px`;
    titleBar.style.height = `${rect.height}px`;
  }
}

// CSS for draggable title bar area
const style = document.createElement("style");
style.textContent = `
  #custom-title-bar {
    app-region: drag; /* Makes area draggable */
    display: flex;
    align-items: center;
    padding: 0 16px;
    background: var(--theme-color);
    color: white;
  }
  
  #custom-title-bar button,
  #custom-title-bar a,
  #custom-title-bar input {
    app-region: no-drag; /* Make interactive elements clickable */
  }
`;
document.head.appendChild(style);

Example: CSS for Window Controls Overlay

/* Environment variables for safe areas */
#app-header {
  /* Use title bar area when available */
  padding-top: env(titlebar-area-height, 0px);
  padding-left: env(titlebar-area-x, 0px);
  padding-right: env(titlebar-area-width, 100%);
}

/* Draggable region for custom title bar */
.title-bar {
  -webkit-app-region: drag;
  app-region: drag;
  height: env(titlebar-area-height, 40px);
  display: flex;
  align-items: center;
  background: var(--theme-color);
}

/* Make buttons clickable in drag region */
.title-bar button,
.title-bar a,
.title-bar input,
.title-bar select {
  -webkit-app-region: no-drag;
  app-region: no-drag;
}

/* Adjust layout when overlay is active */
@media (display-mode: window-controls-overlay) {
  body {
    padding-top: 0;
  }
  
  #custom-title-bar {
    display: flex;
  }
}
Note: Window Controls Overlay enables native-like title bars in desktop PWAs. Set "display_override": ["window-controls-overlay"] in manifest. Use app-region: drag CSS to make areas draggable. Set app-region: no-drag on interactive elements. Experimental - currently only in Chromium-based browsers.

4. App Shortcuts API for Context Menu Integration

Shortcut Property Description
name Name of the shortcut displayed in menu.
short_name Short version of the name (optional).
description Description of what the shortcut does.
url URL to open when shortcut is activated.
icons Array of icon objects for the shortcut.

Example: Define app shortcuts in manifest

{
  "name": "Task Manager Pro",
  "short_name": "TaskPro",
  "start_url": "/",
  "display": "standalone",
  "shortcuts": [
    {
      "name": "New Task",
      "short_name": "New",
      "description": "Create a new task",
      "url": "/new-task?source=shortcut",
      "icons": [
        {
          "src": "/icons/new-task-96x96.png",
          "sizes": "96x96",
          "type": "image/png"
        }
      ]
    },
    {
      "name": "My Tasks",
      "short_name": "Tasks",
      "description": "View my tasks",
      "url": "/my-tasks?source=shortcut",
      "icons": [
        {
          "src": "/icons/my-tasks-96x96.png",
          "sizes": "96x96",
          "type": "image/png"
        }
      ]
    },
    {
      "name": "Calendar",
      "short_name": "Calendar",
      "description": "View calendar",
      "url": "/calendar?source=shortcut",
      "icons": [
        {
          "src": "/icons/calendar-96x96.png",
          "sizes": "96x96",
          "type": "image/png"
        }
      ]
    },
    {
      "name": "Settings",
      "short_name": "Settings",
      "description": "Open settings",
      "url": "/settings?source=shortcut",
      "icons": [
        {
          "src": "/icons/settings-96x96.png",
          "sizes": "96x96",
          "type": "image/png"
        }
      ]
    }
  ]
}

Example: Handle shortcut navigation

// Detect if app was opened from shortcut
const urlParams = new URLSearchParams(window.location.search);
const source = urlParams.get("source");

if (source === "shortcut") {
  console.log("App opened from shortcut");
  
  // Track shortcut usage
  const path = window.location.pathname;
  analytics.track("shortcut_used", { path });
  
  // Handle specific shortcut actions
  if (path === "/new-task") {
    // Open new task modal
    openNewTaskModal();
  } else if (path === "/my-tasks") {
    // Navigate to tasks view
    navigateToTasks();
  } else if (path === "/calendar") {
    // Navigate to calendar view
    navigateToCalendar();
  }
}

// Alternative: Use URL hash for shortcut routing
window.addEventListener("load", () => {
  const hash = window.location.hash;
  
  switch (hash) {
    case "#new-task":
      openNewTaskModal();
      break;
    case "#my-tasks":
      navigateToTasks();
      break;
    case "#calendar":
      navigateToCalendar();
      break;
    default:
      // Show default view
      showDashboard();
  }
});
Note: App Shortcuts appear in OS context menus (right-click on app icon, taskbar, etc.). Maximum 4 shortcuts recommended. Icons should be 96x96 minimum. URLs can include query parameters to track source. Supported on Windows, macOS, Android.

5. Display Mode Detection and Handling

Display Mode Description
fullscreen Full screen without any browser UI.
standalone Standalone app without browser chrome (recommended for PWAs).
minimal-ui Standalone with minimal browser UI (back/forward buttons).
browser Regular browser tab.
window-controls-overlay Desktop PWA with custom title bar.

Example: Detect and respond to display mode

// Check current display mode
function getDisplayMode() {
  // Check each display mode
  const modes = [
    "fullscreen",
    "standalone",
    "minimal-ui",
    "browser"
  ];
  
  for (const mode of modes) {
    if (window.matchMedia(`(display-mode: ${mode})`).matches) {
      return mode;
    }
  }
  
  return "browser"; // Default
}

// Use on page load
const displayMode = getDisplayMode();
console.log("Display mode:", displayMode);

// Apply mode-specific styles or features
switch (displayMode) {
  case "fullscreen":
    console.log("Running in fullscreen mode");
    hideNavigationControls();
    break;
    
  case "standalone":
    console.log("Running as installed PWA");
    showPWAFeatures();
    hideInstallButton();
    break;
    
  case "minimal-ui":
    console.log("Running with minimal UI");
    adjustLayoutForMinimalUI();
    break;
    
  case "browser":
    console.log("Running in browser");
    showInstallPrompt();
    break;
}

// Listen for display mode changes
const displayModeQueries = {
  fullscreen: window.matchMedia("(display-mode: fullscreen)"),
  standalone: window.matchMedia("(display-mode: standalone)"),
  minimalUi: window.matchMedia("(display-mode: minimal-ui)"),
  browser: window.matchMedia("(display-mode: browser)")
};

Object.keys(displayModeQueries).forEach(mode => {
  displayModeQueries[mode].addEventListener("change", (event) => {
    if (event.matches) {
      console.log(`Display mode changed to: ${mode}`);
      handleDisplayModeChange(mode);
    }
  });
});

function handleDisplayModeChange(mode) {
  // Update UI based on new display mode
  document.body.dataset.displayMode = mode;
  
  // Trigger layout recalculation
  window.dispatchEvent(new Event("displaymodechange"));
}

Example: CSS for different display modes

/* Default browser mode styles */
.app-header {
  display: flex;
  padding: 1rem;
}

/* Standalone mode (installed PWA) */
@media (display-mode: standalone) {
  .app-header {
    padding-top: env(safe-area-inset-top);
    background: var(--theme-color);
  }
  
  .install-button {
    display: none; /* Hide install button when installed */
  }
  
  .pwa-features {
    display: block; /* Show PWA-specific features */
  }
}

/* Fullscreen mode */
@media (display-mode: fullscreen) {
  .app-header {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 1000;
  }
  
  .navigation-controls {
    display: flex; /* Show custom nav controls */
  }
}

/* Minimal UI mode */
@media (display-mode: minimal-ui) {
  .app-header {
    padding-top: 0.5rem;
  }
}

/* Browser mode */
@media (display-mode: browser) {
  .pwa-features {
    display: none;
  }
  
  .install-promotion {
    display: block;
  }
}

/* Combine with other media queries */
@media (display-mode: standalone) and (max-width: 768px) {
  /* Standalone mobile styles */
  .app-header {
    flex-direction: column;
  }
}
Note: Display mode indicates how PWA is being viewed. Use matchMedia("(display-mode: MODE)") to detect mode. Apply mode-specific features and styles. Common pattern: hide install button in standalone mode, show custom navigation in fullscreen.

6. Install Events and App Lifecycle Management

Event Description
beforeinstallprompt Fired when browser wants to show install prompt.
appinstalled Fired when PWA is successfully installed.
DOMContentLoaded Fired when initial HTML is loaded and parsed.
load Fired when all resources are loaded.
visibilitychange Fired when page visibility changes (app backgrounded/foregrounded).
pagehide / pageshow Fired when page is hidden/shown (mobile app switching).

Example: Complete PWA lifecycle management

class PWALifecycleManager {
  constructor() {
    this.isInstalled = this.checkInstalled();
    this.installPrompt = null;
    
    this.init();
  }
  
  init() {
    // Installation events
    window.addEventListener("beforeinstallprompt", this.handleBeforeInstall.bind(this));
    window.addEventListener("appinstalled", this.handleAppInstalled.bind(this));
    
    // Lifecycle events
    document.addEventListener("visibilitychange", this.handleVisibilityChange.bind(this));
    window.addEventListener("pageshow", this.handlePageShow.bind(this));
    window.addEventListener("pagehide", this.handlePageHide.bind(this));
    
    // Network events
    window.addEventListener("online", this.handleOnline.bind(this));
    window.addEventListener("offline", this.handleOffline.bind(this));
    
    // Focus events
    window.addEventListener("focus", this.handleFocus.bind(this));
    window.addEventListener("blur", this.handleBlur.bind(this));
  }
  
  checkInstalled() {
    // Check if running in standalone mode
    return window.matchMedia("(display-mode: standalone)").matches ||
           window.navigator.standalone === true;
  }
  
  handleBeforeInstall(event) {
    console.log("Install prompt available");
    event.preventDefault();
    this.installPrompt = event;
    
    // Show custom install UI
    this.showInstallUI();
    
    // Track
    this.trackEvent("install_prompt_shown");
  }
  
  async showInstallPrompt() {
    if (!this.installPrompt) {
      console.log("No install prompt available");
      return;
    }
    
    // Show prompt
    this.installPrompt.prompt();
    
    // Wait for user choice
    const result = await this.installPrompt.userChoice;
    
    if (result.outcome === "accepted") {
      this.trackEvent("install_accepted");
    } else {
      this.trackEvent("install_dismissed");
    }
    
    this.installPrompt = null;
  }
  
  handleAppInstalled(event) {
    console.log("App installed successfully");
    this.isInstalled = true;
    
    // Hide install UI
    this.hideInstallUI();
    
    // Track installation
    this.trackEvent("app_installed");
    
    // Show welcome message
    this.showWelcomeMessage();
  }
  
  handleVisibilityChange() {
    if (document.hidden) {
      console.log("App went to background");
      this.onBackground();
    } else {
      console.log("App came to foreground");
      this.onForeground();
    }
  }
  
  handlePageShow(event) {
    console.log("Page shown");
    
    // Check if restored from cache
    if (event.persisted) {
      console.log("Page restored from bfcache");
      this.onPageRestore();
    }
  }
  
  handlePageHide(event) {
    console.log("Page hidden");
    this.saveState();
  }
  
  handleOnline() {
    console.log("Connection restored");
    this.onOnline();
    this.showNotification("You're back online");
  }
  
  handleOffline() {
    console.log("Connection lost");
    this.onOffline();
    this.showNotification("You're offline. Some features may be limited.");
  }
  
  handleFocus() {
    console.log("App focused");
    this.checkForUpdates();
  }
  
  handleBlur() {
    console.log("App lost focus");
  }
  
  // Lifecycle hooks
  onBackground() {
    // Pause non-critical operations
    this.pauseTimers();
    this.trackEvent("app_backgrounded");
  }
  
  onForeground() {
    // Resume operations
    this.resumeTimers();
    this.refreshData();
    this.trackEvent("app_foregrounded");
  }
  
  onPageRestore() {
    // Refresh stale data
    this.refreshData();
  }
  
  onOnline() {
    // Sync pending changes
    this.syncPendingData();
  }
  
  onOffline() {
    // Switch to offline mode
    this.enableOfflineMode();
  }
  
  saveState() {
    // Save current state
    const state = {
      timestamp: Date.now(),
      route: window.location.pathname,
      scrollPosition: window.scrollY
    };
    
    localStorage.setItem("app_state", JSON.stringify(state));
  }
  
  restoreState() {
    const savedState = localStorage.getItem("app_state");
    if (savedState) {
      const state = JSON.parse(savedState);
      // Restore scroll position, etc.
      window.scrollTo(0, state.scrollPosition);
    }
  }
  
  trackEvent(eventName, data = {}) {
    if (window.analytics) {
      window.analytics.track(eventName, {
        ...data,
        isInstalled: this.isInstalled,
        displayMode: this.getDisplayMode()
      });
    }
  }
  
  getDisplayMode() {
    const modes = ["fullscreen", "standalone", "minimal-ui", "browser"];
    for (const mode of modes) {
      if (window.matchMedia(`(display-mode: ${mode})`).matches) {
        return mode;
      }
    }
    return "browser";
  }
}

// Initialize
const pwaLifecycle = new PWALifecycleManager();

// Usage
document.getElementById("install-button").addEventListener("click", () => {
  pwaLifecycle.showInstallPrompt();
});

Example: Update notification for PWA

// Service worker update detection
let newWorker;

navigator.serviceWorker.register("/sw.js").then(registration => {
  // Check for updates periodically
  setInterval(() => {
    registration.update();
  }, 60 * 60 * 1000); // Check every hour
  
  // Listen for updates
  registration.addEventListener("updatefound", () => {
    newWorker = registration.installing;
    
    newWorker.addEventListener("statechange", () => {
      if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
        // New version available
        showUpdateNotification();
      }
    });
  });
});

// Show update notification
function showUpdateNotification() {
  const notification = document.createElement("div");
  notification.className = "update-notification";
  notification.innerHTML = `
    <p>A new version is available!</p>
    <button onclick="updateApp()">Update Now</button>
    <button onclick="dismissUpdate()">Later</button>
  `;
  document.body.appendChild(notification);
}

// Apply update
function updateApp() {
  if (newWorker) {
    newWorker.postMessage({ type: "SKIP_WAITING" });
  }
  
  // Reload page when new worker takes control
  navigator.serviceWorker.addEventListener("controllerchange", () => {
    window.location.reload();
  });
}

// In service worker (sw.js)
self.addEventListener("message", (event) => {
  if (event.data.type === "SKIP_WAITING") {
    self.skipWaiting();
  }
});
Note: PWA lifecycle includes install, background, foreground, and network events. Use visibilitychange to detect background/foreground. Handle online/offline events for connectivity. Save state on pagehide, restore on pageshow. Check for updates on focus.
Warning: iOS Safari has limited PWA lifecycle events. beforeinstallprompt not available on iOS. Use visibilitychange instead of pagehide/pageshow on some browsers. Test lifecycle thoroughly on target platforms.

Progressive Web App APIs Best Practices

  • Web App Manifest: Include all required fields (name, icons, start_url, display)
  • Provide multiple icon sizes (72px to 512px) and maskable icons for Android
  • Use HTTPS - required for PWA features and service workers
  • Implement custom install flow with beforeinstallprompt for better UX
  • Don't show install prompt immediately - wait for user engagement
  • Track install metrics: prompt shown, accepted, dismissed, installed
  • Detect display mode and adjust UI accordingly (hide install button when installed)
  • Window Controls Overlay: Use for native-like desktop experience
  • App Shortcuts: Provide quick access to key features (max 4 recommended)
  • Handle lifecycle events: visibility change, online/offline, focus/blur
  • Implement update notification when new version available
  • Save/restore app state on page hide/show for better UX
  • Test on all target platforms - iOS Safari has different PWA behavior