Svelte-Specific PWA Patterns
Svelte stores for PWA state
Section titled “Svelte stores for PWA state”Svelte’s store contract — any object with a subscribe method — is the perfect home for PWA lifecycle events. Events like going offline, a new SW becoming available, or a user dismissing an install prompt happen asynchronously and at unpredictable times. Stores make that state reactive and available anywhere in the component tree without prop drilling.
Online / offline store
Section titled “Online / offline store”import { readable } from 'svelte/store';
export const online = readable( typeof navigator !== 'undefined' ? navigator.onLine : true, (set) => { const handleOnline = () => set(true); const handleOffline = () => set(false);
window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline);
return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; });Use it in any component with the $ auto-subscribe shorthand:
<script> import { online } from '$lib/stores/network';</script>
{#if !$online} <div class="offline-banner">You are offline — showing cached content.</div>{/if}The typeof navigator !== 'undefined' guard is required because SvelteKit runs components on the server during SSR, where navigator does not exist.
Install-prompt store and component
Section titled “Install-prompt store and component”The beforeinstallprompt event fires when the browser decides your PWA is installable. You must capture the event object — it is ephemeral — and call .prompt() on it later when the user interacts with your install button.
import { writable } from 'svelte/store';
type BeforeInstallPromptEvent = Event & { prompt: () => Promise<void>; userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;};
export const installPromptEvent = writable<BeforeInstallPromptEvent | null>(null);
if (typeof window !== 'undefined') { window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); // Prevent the mini-infobar installPromptEvent.set(e as BeforeInstallPromptEvent); });}A component that uses the store to show an install button:
<script lang="ts"> import { installPromptEvent } from '$lib/stores/install-prompt';
async function install() { if (!$installPromptEvent) return; await $installPromptEvent.prompt(); const { outcome } = await $installPromptEvent.userChoice; if (outcome === 'accepted') { installPromptEvent.set(null); // hide the button after install } }</script>
{#if $installPromptEvent} <button on:click={install}>Install App</button>{/if}Reactive “update available” state
Section titled “Reactive “update available” state”You can keep update state in a plain writable store so any part of your UI can react to it:
import { writable } from 'svelte/store';
export const updateAvailable = writable(false);export let swRegistration: ServiceWorkerRegistration | null = null;
export async function initSW() { if (!('serviceWorker' in navigator)) return;
const registration = await navigator.serviceWorker.register( '/service-worker.js', { type: import.meta.env.DEV ? 'module' : 'classic' } ); swRegistration = registration;
registration.addEventListener('updatefound', () => { const newWorker = registration.installing; if (!newWorker) return; newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { updateAvailable.set(true); } }); });}
export function applyUpdate() { swRegistration?.waiting?.postMessage({ type: 'SKIP_WAITING' }); window.location.reload();}Wire the stores in src/routes/+layout.svelte:
<script lang="ts"> import { onMount } from 'svelte'; import { initSW, updateAvailable, applyUpdate } from '$lib/stores/sw-update';
onMount(() => { initSW(); });</script>
<slot />
{#if $updateAvailable} <div class="update-banner"> A new version is available. <button on:click={applyUpdate}>Update now</button> </div>{/if}SSR and adapter notes
Section titled “SSR and adapter notes”Service workers only run in the browser. Two common SvelteKit pitfalls:
navigator/windowin store initializers. Always guard withtypeof window !== 'undefined'or run the code insideonMount.adapter-staticfor fully offline apps. If you want users to open every page while offline, every route must be prerendered. Setexport const prerender = truein your root+layout.js(or useadapter-staticwhich forces prerendering). The SW then precaches all prerendered pages via theprerenderedexport from$service-worker.
// src/routes/+layout.js — opt entire app into prerenderingexport const prerender = true;With adapter-node or adapter-vercel, only routes you explicitly prerender are available offline. Dynamic server-rendered routes require a network connection.