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 function 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]
})
function 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()
function sendToast() {
  pushToast({
    title: 'Hello',
    text: 'This is a toast.',
    type: 'success'
  })
}
</script>

<template>
  <div>
    <button @click="sendToast">
      Send toast
    </button>
  </div>
</template>