DEV Community

Cover image for Tired of Vue toast libraries, so I built my own (headless, Vue 3, TS-first)
Adrián Janočko
Adrián Janočko

Posted on • Originally published at reddit.com

Tired of Vue toast libraries, so I built my own (headless, Vue 3, TS-first)

Hey folks đź‘‹ author here, looking for feedback.

I recently needed a toast system for a Vue 3 app that was:

  • modern,
  • lightweight,
  • and didn’t fight my custom styling.

I tried several Vue toast libraries and kept hitting the same issues: a lot of them were Vue 2–only or basically unmaintained, the styling was hard-wired instead of properly themeable, some were missing pretty basic options, and almost none gave me predictable behavior for things like duplicates, timers, or multiple stacks.

So I ended up building my own: Toastflow (core engine) + vue-toastflow (Vue 3 renderer).

What it is

  • Headless toast engine + Vue 3 renderer
  • Toastflow keeps state in a tiny, framework-agnostic store (toastflow-core), and vue-toastflow is just a renderer on top with <ToastContainer /> + a global toast helper.
  • CSS-first theming
  • The default look is driven by CSS variables (including per-type colors like --success-bg, --error-text, etc.). You can swap the design by editing one file or aligning it with your Tailwind/daisyUI setup.
  • Smooth stack animations
  • Enter/leave + move animations when items above/below are removed, for all positions (top-left, top-center, top-right, bottom-left, bottom-center, bottom-right). Implemented with TransitionGroup and overridable via animation config.
  • Typed API, works inside and outside components
  • You install the plugin once, then import toast from anywhere (components, composables, services, plain TS modules). Typed helpers: toast.show, toast.success, toast.error, toast.warning, toast.info, toast.loading, toast.update, toast.dismiss, toast.dismissAll, etc.
  • Deterministic behavior
  • The core handles duplicates, timers, pause-on-hover, close-on-click, maxVisible, stack order (newest/oldest), and clear-all in a predictable way.
  • Extras
  • Promise/async flows (toast.loading), optional HTML content with supportHtml, lifecycle hooks, events (toast.subscribeEvents), timestamps (showCreatedAt, createdAtFormatter), and a headless slot API if you want to render your own card.

Quick taste

    // main.ts
    import { createApp } from 'vue'
    import App from './App.vue'
    import { createToastflow, ToastContainer } from 'vue-toastflow'

    const app = createApp(App)

    app.use(
      createToastflow({
        // optional global defaults
        position: 'top-right',
        duration: 5000,
      }),
    )

    // register globally or import locally where you render it    
    app.component('ToastContainer', ToastContainer)

    app.mount('#app')
Enter fullscreen mode Exit fullscreen mode
    <!-- Somewhere in your app -->
    <script setup lang="ts">
    import { toast } from 'vue-toastflow'

    function handleSave() {
      toast.success({
        title: 'Saved',
        description: 'Your changes have been stored.',
      })
    }
    </script>

    <template>
      <button @click="handleSave">Save</button>
      <ToastContainer />
    </template>
Enter fullscreen mode Exit fullscreen mode

Links

Top comments (0)