How to Implement Toast Notifications in Vue
You need to show users feedback—a saved form, a failed API call, a successful upload. Toast notifications solve this elegantly: brief messages that appear, deliver information, and disappear without disrupting workflow.
This guide covers two approaches to Vue 3 toast notifications: building a custom composable-based system and integrating established libraries like Vue Toastification or Notivue. Both use modern Composition API patterns with <script setup> syntax.
Key Takeaways
- A custom toast system in Vue 3 needs just three pieces: shared reactive state, a composable function, and a Teleport-based container component.
- Third-party libraries like Vue Toastification and Notivue handle queue management, animations, accessibility, and theming out of the box.
- In Nuxt 3, toast notifications require client-only execution—use
<ClientOnly>wrappers or.client.tsplugin suffixes. - Accessible toasts use
aria-liveregions, dismiss buttons with clear labels, and respectprefers-reduced-motionpreferences.
Building a Custom Toast System with Vue Composables
A minimal Vue composable notification system requires three pieces: reactive state, a composable function, and a rendered component using Teleport.
The Toast Composable
Create a shared reactive store that any component can access:
// composables/useToast.ts
import { ref, readonly } from 'vue'
interface Toast {
id: number
message: string
type: 'success' | 'error' | 'info' | 'warning'
}
const toasts = ref<Toast[]>([])
let id = 0
export function useToast() {
const add = (message: string, type: Toast['type'] = 'info', duration = 3000) => {
const toast = { id: ++id, message, type }
toasts.value.push(toast)
if (duration > 0) {
setTimeout(() => remove(toast.id), duration)
}
}
const remove = (toastId: number) => {
toasts.value = toasts.value.filter(t => t.id !== toastId)
}
return {
toasts: readonly(toasts),
success: (msg: string) => add(msg, 'success'),
error: (msg: string) => add(msg, 'error'),
info: (msg: string) => add(msg, 'info'),
warning: (msg: string) => add(msg, 'warning'),
remove
}
}
The toasts ref is declared outside the composable function so that every call to useToast() shares the same reactive array. The readonly wrapper prevents consumers from mutating state directly—only add and remove can modify the list.
The Toast Container Component
Render toasts using Teleport to position them outside your component tree:
<!-- components/ToastContainer.vue -->
<script setup lang="ts">
import { useToast } from '@/composables/useToast'
const { toasts, remove } = useToast()
</script>
<template>
<Teleport to="body">
<div
class="toast-container"
role="region"
aria-live="polite"
aria-label="Notifications"
>
<div
v-for="toast in toasts"
:key="toast.id"
:class="['toast', `toast--${toast.type}`]"
role="status"
>
{{ toast.message }}
<button @click="remove(toast.id)" aria-label="Dismiss notification">×</button>
</div>
</div>
</Teleport>
</template>
Place <ToastContainer /> in your App.vue, then call useToast() from any component to trigger notifications.
Note on ARIA roles: The container uses
aria-live="polite"so screen readers announce new toasts without interrupting the user. Individual toasts userole="status"rather thanrole="alert", which pairs correctly with the polite live region. Reserverole="alert"(which impliesaria-live="assertive") for urgent error messages that demand immediate attention.
Using Third-Party Libraries
For production applications, maintained libraries handle edge cases you’d otherwise build yourself: queue management, animations, accessibility, and theming.
Vue Toastification Setup
Install and register the plugin:
// main.ts
import { createApp } from 'vue'
import Toast from 'vue-toastification'
import 'vue-toastification/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.use(Toast, {
position: 'top-right',
timeout: 5000
})
app.mount('#app')
Use the composable in components:
<script setup>
import { useToast } from 'vue-toastification'
const toast = useToast()
const handleSave = async () => {
try {
await saveData()
toast.success('Changes saved')
} catch {
toast.error('Save failed')
}
}
</script>
Discover how at OpenReplay.com.
Notivue Alternative
Notivue offers a similar composable API with additional customization options and built-in accessibility features.
Toast Notifications in Nuxt 3
Toast notifications in Nuxt 3 must be triggered in client-side contexts (for example, in event handlers or onMounted) because they manipulate the DOM and should not run during server-side rendering:
<!-- app.vue -->
<template>
<NuxtPage />
<ClientOnly>
<ToastContainer />
</ClientOnly>
</template>
If you’re using Nuxt UI, it provides its own useToast composable—no additional library needed. For other setups, create a Nuxt plugin:
// plugins/toast.client.ts
import Toast from 'vue-toastification'
import 'vue-toastification/dist/index.css'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(Toast)
})
The .client.ts suffix ensures the plugin only runs in the browser.
Accessibility Considerations
Toasts must be accessible to screen reader users:
- Use
aria-live="polite"on the toast container so new messages are announced without interruption - Provide dismiss buttons with descriptive
aria-labelattributes - Respect
prefers-reduced-motionby disabling animations when requested - Avoid auto-dismissing error messages—users need time to read and act on them
@media (prefers-reduced-motion: reduce) {
.toast {
animation: none
}
}
Conclusion
Build a custom toast system when you need minimal bundle size or highly specific behavior. Use Vue Toastification or Notivue when you want proven accessibility, animations, and configuration options without maintenance overhead.
Both approaches work in Vue 3 and Nuxt 3. The composable pattern keeps your code testable and your components decoupled from notification logic. Start with the custom composable to understand the mechanics, then evaluate whether a library better serves your project’s long-term needs.
FAQs
Yes. Because the toasts ref is declared outside the useToast function at the module level, every component that calls useToast shares the same reactive state. This means any component can trigger or dismiss toasts without passing props or emitting events through parent components.
Teleport moves the toast DOM nodes to the body element, outside your component hierarchy. This prevents parent CSS like overflow hidden or z-index stacking contexts from clipping or hiding your toasts. It also keeps toast positioning consistent regardless of where the container component is mounted.
In the custom composable, pass zero or a negative number as the duration argument to skip the setTimeout call. With Vue Toastification, set timeout to false for specific toasts. Error messages should persist until the user manually dismisses them so they have enough time to read and respond.
Yes. Toast notifications manipulate the DOM, which is unavailable during server-side rendering. Wrap your toast container in a ClientOnly component or register your toast library as a client-only Nuxt plugin using the .client.ts file suffix. This ensures toast logic only executes in the browser.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.