Back

How to Implement Toast Notifications in Vue

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.ts plugin suffixes.
  • Accessible toasts use aria-live regions, dismiss buttons with clear labels, and respect prefers-reduced-motion preferences.

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 use role="status" rather than role="alert", which pairs correctly with the polite live region. Reserve role="alert" (which implies aria-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>

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-label attributes
  • Respect prefers-reduced-motion by 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.

OpenReplay