Progressive Web Apps (PWA)

Offline Support

18 min Lesson 6 of 30

Offline Support in PWAs

One of the most powerful features of Progressive Web Apps is the ability to work offline. By implementing proper offline support, your app can provide a seamless experience even when the user loses network connectivity.

Detecting Offline Status

JavaScript provides the navigator.onLine property to check network connectivity status:

<script> // Check current online status if (navigator.onLine) { console.log('Connected to the internet'); } else { console.log('No internet connection'); } // Listen for online/offline events window.addEventListener('online', () => { console.log('Connection restored'); updateUIForOnlineMode(); }); window.addEventListener('offline', () => { console.log('Connection lost'); updateUIForOfflineMode(); }); function updateUIForOnlineMode() { document.body.classList.remove('offline-mode'); document.body.classList.add('online-mode'); showNotification('You are back online!', 'success'); } function updateUIForOfflineMode() { document.body.classList.remove('online-mode'); document.body.classList.add('offline-mode'); showNotification('You are offline. Some features may be limited.', 'warning'); } </script>
Warning: The navigator.onLine property is not 100% reliable. It only detects if there's a network connection, not if the internet is actually accessible. A device might be connected to a WiFi network that has no internet access, and navigator.onLine would still return true.

Creating an Offline Page

A common pattern is to cache a dedicated offline page that is shown when the user tries to navigate while offline:

// In your service worker (sw.js) const OFFLINE_PAGE = '/offline.html'; const CACHE_NAME = 'offline-v1'; self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => { return cache.addAll([ OFFLINE_PAGE, '/styles/offline.css', '/images/offline-icon.svg' ]); }) ); self.skipWaiting(); }); self.addEventListener('fetch', (event) => { // Only handle navigate requests (HTML pages) if (event.request.mode === 'navigate') { event.respondWith( fetch(event.request).catch(() => { return caches.match(OFFLINE_PAGE); }) ); } });

Offline Page HTML Example

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>You're Offline</title> <link rel="stylesheet" href="/styles/offline.css"> </head> <body> <div class="offline-container"> <img src="/images/offline-icon.svg" alt="Offline"> <h1>You're Offline</h1> <p>It looks like you've lost your internet connection.</p> <p>Don't worry, you can still access previously viewed content.</p> <button onclick="window.history.back()">Go Back</button> <button onclick="location.reload()">Try Again</button> </div> </body> </html>

Graceful Degradation Strategies

When implementing offline support, consider these UX patterns:

Best Practices for Offline UX:
  • Visual Indicators: Show a banner or icon when offline
  • Disable Features: Gray out or hide features that require connectivity
  • Queue Actions: Save user actions locally and sync when online
  • Cached Content: Show previously loaded content with timestamps
  • Helpful Messages: Explain what works and what doesn't offline

Offline Data Storage

Store data locally for offline access using various storage mechanisms:

// Using localStorage for simple data function saveOfflineData(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); console.log('Data saved for offline use'); } catch (e) { console.error('Failed to save offline data:', e); } } function getOfflineData(key) { try { const data = localStorage.getItem(key); return data ? JSON.parse(data) : null; } catch (e) { console.error('Failed to retrieve offline data:', e); return null; } } // Check online status before fetching async function fetchWithOfflineFallback(url, storageKey) { if (navigator.onLine) { try { const response = await fetch(url); const data = await response.json(); saveOfflineData(storageKey, data); return data; } catch (error) { console.warn('Fetch failed, using cached data'); return getOfflineData(storageKey); } } else { return getOfflineData(storageKey); } }

Advanced Offline Detection

For more reliable offline detection, implement a heartbeat check:

class ConnectionMonitor { constructor(checkInterval = 30000) { this.checkInterval = checkInterval; this.isOnline = navigator.onLine; this.listeners = []; this.init(); } init() { // Listen to browser events window.addEventListener('online', () => this.updateStatus(true)); window.addEventListener('offline', () => this.updateStatus(false)); // Periodic heartbeat check setInterval(() => this.checkConnection(), this.checkInterval); } async checkConnection() { try { const response = await fetch('/ping', { method: 'HEAD', cache: 'no-cache' }); this.updateStatus(response.ok); } catch (error) { this.updateStatus(false); } } updateStatus(isOnline) { if (this.isOnline !== isOnline) { this.isOnline = isOnline; this.listeners.forEach(callback => callback(isOnline)); } } onChange(callback) { this.listeners.push(callback); } } // Usage const monitor = new ConnectionMonitor(); monitor.onChange((isOnline) => { console.log(isOnline ? 'Online' : 'Offline'); updateUI(isOnline); });
Note: The heartbeat check adds network overhead. Use a reasonable interval (30-60 seconds) to balance responsiveness with resource usage.

Offline-First Architecture

In an offline-first approach, the app always tries to load from cache first, then updates from the network:

// Service Worker: Cache-first strategy self.addEventListener('fetch', (event) => { event.respondWith( caches.match(event.request).then((cachedResponse) => { // Return cached version if available if (cachedResponse) { // Update cache in background fetch(event.request).then((networkResponse) => { caches.open(CACHE_NAME).then((cache) => { cache.put(event.request, networkResponse); }); }); return cachedResponse; } // Otherwise fetch from network return fetch(event.request).then((networkResponse) => { // Cache the new response return caches.open(CACHE_NAME).then((cache) => { cache.put(event.request, networkResponse.clone()); return networkResponse; }); }); }) ); });
Exercise:
  1. Create an offline.html page with custom styling
  2. Update your service worker to cache and serve the offline page
  3. Add visual indicators in your app to show online/offline status
  4. Implement localStorage fallback for API data
  5. Test by disabling your network connection