Push notifications in Vue 3 and Nuxt
Vue doesn’t ship a push API of its own — and doesn’t need to. The browser’s Push API works the same in any framework. Here’s a clean Vue 3 composable, where the service worker goes in Vite and Nuxt, and how to use vite-plugin-pwa without losing push.
- Put sw.js in public/ (Vite and Nuxt serve it at /sw.js).
- Wrap permission, subscription and state in a usePush() composable; call subscribe() from a click.
- With vite-plugin-pwa, use the injectManifest strategy to add push listeners to your own worker.
- Nuxt: add scripts via app.head in nuxt.config, and guard browser-only code with onMounted.
1. A usePush composable
// composables/usePush.js
import { ref, onMounted } from 'vue';
const VAPID_PUBLIC = import.meta.env.VITE_VAPID_PUBLIC;
export function usePush() {
const state = ref('loading'); // unsupported | default | denied | subscribed
onMounted(async () => {
if (!('serviceWorker' in navigator) || !('PushManager' in window)) return (state.value = 'unsupported');
const reg = await navigator.serviceWorker.register('/sw.js');
state.value = (await reg.pushManager.getSubscription()) ? 'subscribed' : Notification.permission;
});
async function subscribe() {
const reg = await navigator.serviceWorker.ready;
if (await Notification.requestPermission() !== 'granted') return (state.value = Notification.permission);
const sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: VAPID_PUBLIC });
await fetch('/api/push/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(sub) });
state.value = 'subscribed';
}
return { state, subscribe };
}<!-- NotifyButton.vue -->
<script setup>
import { usePush } from '@/composables/usePush';
const { state, subscribe } = usePush();
</script>
<template>
<button v-if="state === 'default'" @click="subscribe">🔔 Get notifications</button>
<p v-else-if="state === 'denied'">Notifications are blocked in your browser settings.</p>
</template>2. The service worker
Files in public/ are copied as-is to the site root in both Vite and Nuxt, so public/sw.js becomes /sw.js with scope /:
// sw.js — show the notification and open the link on click
self.addEventListener('push', e => {
const d = e.data ? e.data.json() : {};
e.waitUntil(self.registration.showNotification(d.title || 'Update', { body: d.body, icon: '/icon-192.png', data: { url: d.url || '/' } }));
});
self.addEventListener('notificationclick', e => {
e.notification.close();
e.waitUntil(clients.openWindow(e.notification.data.url));
});Using vite-plugin-pwa
The default generateSW strategy builds a Workbox worker for caching and doesn’t include your push handlers. Switch to injectManifest and write the worker yourself:
// vite.config.js
import { VitePWA } from 'vite-plugin-pwa';
export default { plugins: [VitePWA({ strategies: 'injectManifest', srcDir: 'src', filename: 'sw.js', registerType: 'autoUpdate' })] };
// src/sw.js
import { precacheAndRoute } from 'workbox-precaching';
precacheAndRoute(self.__WB_MANIFEST);
self.addEventListener('push', e => { /* same as above */ });For Nuxt, @vite-pwa/nuxt exposes the same options.
Nuxt specifics
- Browser APIs only exist on the client: call them in
onMountedor a.client.jsplugin. - Add third-party scripts in
nuxt.config:export default defineNuxtConfig({ app: { head: { script: [{ src: 'https://pusharoo.com/sdk/pusharoo.js', async: true, 'data-site': 'pk_your_site_key' }] } } }); - Put the subscribe endpoint in
server/api/push/subscribe.post.jsif you send from Nitro.
Sending
Any Web Push library works: web-push for Node fits naturally if your Nuxt server or an API sends notifications. Push services answer 201 when they accept a message. 404 or 410 means the subscription is gone — delete it. 413 means the payload is too large (keep it under about 4 KB), 429 means slow down. Details in the Web Push protocol.
Development gotchas
- Vite’s dev server serves
public/sw.jsbut doesn’t bundle it; keep the worker plain JavaScript or use vite-plugin-pwa’s dev options. - Service workers survive reloads — tick “Update on reload” in DevTools while editing.
- Hot module replacement doesn’t reload the worker; push handlers change only after the worker updates.
- Test on
localhostor HTTPS; a LAN IP over HTTP won’t allow service workers.
iPhone-aware UI
const isIOS = /iPhone|iPad/.test(navigator.userAgent);
const installed = matchMedia('(display-mode: standalone)').matches;
const state = computed(() => (isIOS && !installed) ? 'needs-home-screen' : push.state.value);Show Home Screen steps for needs-home-screen rather than a button that fails silently (steps to show).
Sharing state with Pinia
If several components need the subscription state (a header bell, a settings page, an article footer), move the composable’s state into a Pinia store and initialise it once in App.vue or a Nuxt client plugin. That avoids registering the worker or calling getSubscription() repeatedly and keeps the UI consistent after someone subscribes in one place.
When not to build it
Everything above is the do-it-yourself route: you own the keys, storage, sending queue and cleanup. If what you need is marketing-style notifications to anonymous visitors in your Vue or Nuxt app — a branded prompt, segments, schedules, A/B tests, automations, iPhone Home Screen handling — Pusharoo does that for free: add the snippet to your base template and upload sw.js. You can still call Pusharoo’s REST API from Vue or Nuxt for transactional sends (POST /api/v1/notify) and receive signed webhooks when people subscribe, so the two approaches mix well.
With Pusharoo, Pusharoo.subscribe() replaces the composable’s subscribe function and Pusharoo.state() its state — including the special ios-needs-home-screen state for iPhone visitors.
Questions
Does Vue have built-in push notifications?
No; use the browser Push API (as in the composable) or a service. Frameworks don’t change how web push works.
Where should sw.js go in a Vite project?
In public/, so it is served from the site root with full scope.
Can Vue push notifications reach iPhone?
Yes, on iOS 16.4+ once the site is added to the Home Screen and opened from there.
Can I use Firebase Messaging with Vue?
Yes, but it adds a Firebase project and its own service worker. For web-only push, the standard Push API with VAPID keys (as in the composable) is simpler.
How do I add push to a Quasar or Ionic Vue app?
In the web/PWA build, use the Push API as shown. Inside native Capacitor or Cordova shells, use the platform’s native push plugin instead.
Keep reading
Web push notifications in React and Next.js
Service worker in public/, a subscribe hook, a route handler with web-push, env keys and iPhone notes.
Angular push notifications with SwPush
SwPush.requestSubscription, the ngsw payload format, onActionClick navigation and coexisting workers.
Push notifications in SvelteKit
src/service-worker.js vs. static/sw.js, handlers, a subscribe function, a +server.js endpoint and testing.
PWA push notifications on iOS and Android
Manifest, service worker, iPhone Home Screen requirement, Android install, badging and a setup checklist.
More in Developers: The Web Push API in JavaScript: a working example · Service worker push events, done right · VAPID keys for web push, explained · The Web Push protocol, explained · Web push notifications with Node.js · Web push notifications in PHP · Laravel browser push notifications (no Firebase) · Declarative Web Push, explained · Firebase web push vs. standard web push · Web push in Python with pywebpush · Django push notifications to the browser · Web push notifications in Ruby on Rails · Web push in Go with webpush-go · All guides
Free for life. No limits.
Unlimited subscribers, campaigns and automations. No badges. About five minutes to install.