Offline-first PWA with Svelte & Vite

Published on

I’ve always been a big fan of Progressive Web Apps (PWAs). They offer app-like functionality without the development / labor overhead that comes with building a traditional app. Plus, I don’t need to learn new toolsets to develop desktop and mobile applications as a full-stack web developer. was an offline-ready PWA for a long time, when it was still just a single HTML page. Some time ago, however, I rewrote the site using Svelte, the SPA framework, and Routify, the router for Svelte. At that time I tried to implement the site as a progressive web app following the instructions in Routify’s docs but the docs seem to be outdated and the PWA was just broken. (It would show ERR_FAILED on every reload of the page – some kind of issue with the service worker that I didn’t have time to diagnose, unfortunately).

Luckily there’s a much better (and simpler!) option available now. Vite-plugin-PWA enables developers to easily turn apps built with Vite into a PWA, regardless of framework or configuration ability. It’s truly dead simple.

Yep, their docs are a PWA too.

Although their docs are pretty high quality, I will provide an opinionated tutorial on how I implemented Vite-plugin-PWA in my own Svelte app.

Note that my particular setup here is using Routify as the router for my Svelte app. If you’re using something like SvelteKit there are better instructions on the Vite-plugin-PWA website.


Installation is pretty straightforward. It involves installing the npm package:

npm i vite-plugin-pwa -D

Then adding VitePWA to the plugins section of vite.config.js, for example:

import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
  plugins: [
// don't just copy paste this into your vite.config.js!


Now we just need to get Vite-plugin-PWA to play well with Svelte and add some customizations to make the PWA truly shine.

I set the following options for VitePWA in vite.config.js:

            workbox: {
                globPatterns: ['**/*.{js,css,html,svg,png,woff2}'],
            registerType: "autoUpdate",
            manifest: {
                "background_color": "#ffffff",
                "theme_color": "#7E1F86",
                "name": "",
                "short_name": "",
                "start_url": "/",
                "display": "standalone",
                "icons": [
                        "src": "/images/largeicon.png",
                        "sizes": "1000x1000",
                        "type": "image/png",
                        "purpose": "maskable any"

Here’s what these options do:

  • workbox.globPatterns: By default, the glob looks like **/*.{js,css.html}. This means the service worker will automatically cache all js/css/html files for offline use. In this case, I added svg, png, and woff2 so that fonts and images also appear when the PWA is offline.
  • registerType: The service worker won’t update on its own by default. Setting autoUpdate means the service worker will automatically update itself. (This also means that, even though the SW is caching the site, visitors will still see new content almost immediately after it’s published. No need to wait for TTLs to expire.)
  • manifest: It’s possible to set a manifest.json inline in VitePWA’s options. When that’s set, VitePWA will automatically generate a manifest file for your app. Very convenient.

At this point, the app is almost ready to use. Now we just need to call Vite-plugin-PWA’s registerSW function somewhere in the app to register the service worker. It’s possible to do this in main.js but I opted to create a simple component that is imported in my App.svelte (so it’s inserted into every page of my app):

    import {useRegisterSW} from 'virtual:pwa-register/svelte';
    import { slide } from 'svelte/transition';
    const {offlineReady, needRefresh, updateServiceWorker} = useRegisterSW({
        onRegistered(swr) {
            console.log(`SW registered: ${swr}`);
        onRegisterError(error) {
            console.log('SW registration error', error);
        onOfflineReady() {
            console.log('SW ready for offline')
            setTimeout(() => close(), 5000)
    function close() {
    $: toast = $offlineReady || $needRefresh;

{#if toast}
    <div class="fixed right-0 bottom-0 m-4 p-6 rounded shadow z-[100] bg-white border-blue-500 border max-w-sm" in:slide out:slide
        <div class="mb-4">
            {#if $offlineReady}
        This site is ready to use offline.
        A new version of this site is available! Click the reload button to update.
        {#if $needRefresh}
            <button class="button m-0" on:click={() => updateServiceWorker(true)}>
        <button class="button m-0" on:click={close}>

Note: My site uses TailwindCSS so the “offline ready” and “update available” popups are styled with Tailwind utility classes.

Also, if you’re using SSR, you need to make sure the component is not loaded on the server side, otherwise your site will throw errors. There’s a good example of how to do this for SvelteKit here.

This component calls the registerSW function and also adds a nice popup that appears when the site is ready for offline use (and that appears when an update is needed; however, updates are handled automatically because of the registerType setting so it won’t show in this case):

Offline-ready indeed.

At this point, the app should be completely functional as a Progressive Web App. (Note that it will only function as a PWA in production, not in development.)

It will also work offline. Overall, Vite-plugin-PWA offers a super convenient and easy-to-integrate option for turning a standard web app (built with Vite) into a full-fledged Progressive Web App. Highly recommended 🙂

Notify of

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Newest Most Voted
Inline Feedbacks
View all comments