This guide use Pinia as store pinia.vuejs.org and SVG icons use vite-svg-loader, you can use SVG guide for Vue 3 to install it.

Create store

import { defineStore } from 'pinia'
/**
* Manage toats global state
* from: https://blog.aspiresys.pl/technology/toast-toasts-in-vue
*/
export const useToastStore = defineStore('toast', {
state: () => ({
toasts: [] as Toast[],
}),
getters: {
/**
* Get all toast
*/
toastsList: (state): Toast[] => state.toasts,
},
actions: {
/**
* Add toast to list
*/
addToast(payload: Toast) {
const toasts = this.toasts
toasts.push(payload)
this.timeoutToast(payload)
this.$patch({
toasts,
})
},
/**
* Set timeout to toast
*/
timeoutToast(payload: Toast) {
setTimeout(() => {
this.deleteToast(payload)
}, 4000)
},
/**
* Delete a toast
*/
deleteToast(payload: Toast) {
const toasts = this.toasts
const index = toasts.findIndex(key => key.id === payload.id)
toasts.splice(index, 1)
this.$patch({
toasts,
})
},
},
})

Create interface

declare type ToastAuto = 'success' | 'error'
declare type ToastType =
| 'success'
| 'warning'
| 'error'
| 'information'
| 'default'
declare interface Toast {
title: string
text?: string
type?: ToastType
date?: Date
id?: string
}

Create composable (optional)

import { useToastStore } from '~~/store/toast'
/**
* Toast composable
*/
export const useToast = () => {
/**
* Push toast to store
*/
const pushToast = (toast: Toast) => {
const store = useToastStore()
store.addToast(toast)
}
return {
pushToast,
}
}

Create components

<script setup lang="ts">
import AppToast from '@/components/app/toast.vue'
import { useToastStore } from '~/store/toast'
const toastStore = useToastStore()
</script>
<template>
<div class="fixed top-0 right-0 z-50">
<transition-group name="list" tag="section">
<AppToast v-for="(toast, id) in toastStore.toastsList" :key="id" :toast="toast" />
</transition-group>
</div>
</template>
<script setup lang="ts">
import SvgIcon from '@/components/svg-icon.vue'
import { useToastStore } from '~/store/toast'
const props = defineProps<{
toast?: Toast
}>()
const type = props.toast?.type ? props.toast.type : 'default'
const bgColor = computed(() => {
const colors = {
success: 'bg-green-100',
warning: 'bg-orange-100',
error: 'bg-red-100',
information: 'bg-blue-100',
default: 'bg-blue-100',
}
return colors[type] || colors.default
})
const color = computed(() => {
const colors = {
success: 'text-green-400',
warning: 'text-orange-400',
error: 'text-red-400',
information: 'text-blue-400',
default: 'text-blue-400',
}
return colors[type]
})
const clear = () => {
const toast = useToastStore()
toast.deleteToast(props.toast!)
}
</script>
<template>
<section
v-if="toast"
class="pointer-events-auto m-3 w-64 max-w-sm overflow-hidden rounded-lg bg-gray-100 shadow-lg ring-1 ring-black ring-opacity-5 dark:bg-gray-700 sm:w-96 relative"
:class="bgColor"
>
<span
class="absolute inset-x-0 bottom-0 h-1 bg-primary-500 dark:bg-primary-600 animate-life"
/>
<div class="p-4">
<div class="flex items-start">
<div class="shrink-0">
<SvgIcon
:name="toast.type ? `toast-${toast.type}` : 'information'"
:class="color"
class="h-6 w-6"
/>
</div>
<div class="ml-3 w-0 flex-1 pt-0.5">
<p class="text-sm font-medium text-gray-900 dark:text-gray-200">
{{ toast.title }}
</p>
<p class="mt-1 text-sm text-gray">
{{ toast.text }}
</p>
</div>
<div class="ml-4 flex shrink-0">
<button
class="inline-flex rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:bg-gray-800 dark:text-gray-300"
@click="clear"
>
<span class="sr-only">Close</span>
<svg
class="h-5 w-5"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
</div>
</div>
</section>
</template>

With Tailwind CSS, you can animation life

/** @type {import('tailwindcss').Config} */
module.exports = {
theme: {
extend: {
keyframes: {
life: {
'0%': { width: '100%' },
'100%': { width: '0%' },
},
},
animation: {
life: 'life 1900ms linear forwards',
},
},
},
}

Usage

<script setup lang="ts">
import useToast from '~/composables/useToast.ts'
const { pushToast } = useToast()
const sendToast = () => {
pushToast({
title: 'Hello',
text: 'This is a toast.',
type: 'success'
})
}
</script>
<template>
<div>
<button @click="sendToast">
Send toast
</button>
</div>
</template>