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.
Related Articles
For offline functionality, you might also want to learn how to use Vue with Service Workers and how to use Vue with IndexedDB.



