How to add PWA support in Vue

Progressive Web App (PWA) support enables Vue applications to work offline, install to home screen, and provide native-like experiences. As the creator of CoreUI with 12 years of Vue development experience, I’ve built Vue PWAs that serve millions of users with app-like functionality.

The most effective approach is to use the Vite PWA plugin which automatically generates service workers, manifests, and handles caching strategies.

Install Dependencies

Install Vite PWA plugin:

npm install vite-plugin-pwa -D

Configure Vite

Update vite.config.js:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { VitePWA } from 'vite-plugin-pwa'

export default defineConfig({
  plugins: [
    vue(),
    VitePWA({
      registerType: 'autoUpdate',
      includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'mask-icon.svg'],
      manifest: {
        name: 'My Vue PWA',
        short_name: 'VuePWA',
        description: 'My awesome Vue Progressive Web App',
        theme_color: '#4DBA87',
        background_color: '#ffffff',
        display: 'standalone',
        orientation: 'portrait',
        scope: '/',
        start_url: '/',
        icons: [
          {
            src: 'pwa-192x192.png',
            sizes: '192x192',
            type: 'image/png'
          },
          {
            src: 'pwa-512x512.png',
            sizes: '512x512',
            type: 'image/png',
            purpose: 'any maskable'
          }
        ]
      },
      workbox: {
        globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
        runtimeCaching: [
          {
            urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
            handler: 'CacheFirst',
            options: {
              cacheName: 'google-fonts-cache',
              expiration: {
                maxEntries: 10,
                maxAgeSeconds: 60 * 60 * 24 * 365
              },
              cacheableResponse: {
                statuses: [0, 200]
              }
            }
          },
          {
            urlPattern: /^https:\/\/api\.example\.com\/.*/i,
            handler: 'NetworkFirst',
            options: {
              cacheName: 'api-cache',
              expiration: {
                maxEntries: 50,
                maxAgeSeconds: 60 * 5
              },
              cacheableResponse: {
                statuses: [0, 200]
              }
            }
          }
        ]
      }
    })
  ]
})

Register Service Worker

Update src/main.js:

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
app.mount('#app')

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then(registration => {
        console.log('SW registered:', registration)
      })
      .catch(error => {
        console.error('SW registration failed:', error)
      })
  })
}

Create Icons

Generate PWA icons (192x192 and 512x512):

# Place icons in public folder
public/
  ├── pwa-192x192.png
  ├── pwa-512x512.png
  ├── apple-touch-icon.png (180x180)
  └── favicon.ico

Add Install Prompt

Create src/components/InstallPrompt.vue:

<script setup>
import { ref, onMounted } from 'vue'

const deferredPrompt = ref(null)
const showInstall = ref(false)

onMounted(() => {
  window.addEventListener('beforeinstallprompt', (e) => {
    e.preventDefault()
    deferredPrompt.value = e
    showInstall.value = true
  })

  window.addEventListener('appinstalled', () => {
    deferredPrompt.value = null
    showInstall.value = false
  })
})

const install = async () => {
  if (!deferredPrompt.value) return

  deferredPrompt.value.prompt()

  const { outcome } = await deferredPrompt.value.userChoice

  if (outcome === 'accepted') {
    deferredPrompt.value = null
    showInstall.value = false
  }
}

const dismiss = () => {
  showInstall.value = false
}
</script>

<template>
  <div v-if="showInstall" class="install-prompt">
    <p>Install this app for a better experience</p>
    <button @click="install">Install</button>
    <button @click="dismiss">Later</button>
  </div>
</template>

<style scoped>
.install-prompt {
  position: fixed;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  background: white;
  padding: 16px;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  z-index: 1000;
}

button {
  margin: 0 8px;
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

Update Notification

Create src/components/UpdateNotification.vue:

<script setup>
import { useRegisterSW } from 'virtual:pwa-register/vue'

const {
  offlineReady,
  needRefresh,
  updateServiceWorker
} = useRegisterSW()

const close = () => {
  offlineReady.value = false
  needRefresh.value = false
}
</script>

<template>
  <div v-if="offlineReady || needRefresh" class="pwa-toast">
    <div class="message">
      <span v-if="offlineReady">
        App ready to work offline
      </span>
      <span v-else>
        New content available, click reload to update.
      </span>
    </div>

    <button v-if="needRefresh" @click="updateServiceWorker()">
      Reload
    </button>
    <button @click="close">Close</button>
  </div>
</template>

<style scoped>
.pwa-toast {
  position: fixed;
  bottom: 20px;
  right: 20px;
  padding: 16px;
  background: #333;
  color: white;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  z-index: 1000;
}

.message {
  margin-bottom: 12px;
}

button {
  margin-right: 8px;
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

iOS Meta Tags

Add to index.html:

<head>
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
  <meta name="apple-mobile-web-app-title" content="VuePWA">
  <link rel="apple-touch-icon" href="/apple-touch-icon.png">
</head>

Build and Test

Build for production:

npm run build

Test PWA:

npx serve -s dist

Open Chrome DevTools → Lighthouse → Run PWA audit.

Best Practice Note

This is the same PWA configuration we use in CoreUI’s Vue admin templates. Vite PWA plugin handles service worker generation, asset precaching, and update strategies automatically. The install prompt and update notifications provide a native app-like experience.

For production applications, consider using CoreUI’s Vue Admin Template which includes pre-configured PWA support with offline functionality, push notifications, and background sync.

For offline functionality, you might also want to learn how to use Vue with Service Workers and how to use Vue with IndexedDB.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.

Answers by CoreUI Core Team