Skip to content

@vibe-labs/design-vue-toasts

Vue 3 toast notification system for the Vibe Design System. Imperative API with variant shortcuts, promise support, pause-on-hover, and stack/queue display modes.

Installation

ts
import { toast, VibeToastContainer } from "@vibe-labs/design-vue-toasts";

Requires the CSS layer from @vibe-labs/design-components-toasts.


Setup

Add the container once at the root of your app:

vue
<!-- App.vue -->
<template>
  <RouterView />
  <VibeToastContainer position="top-right" />
</template>

Then fire toasts from anywhere — no provide/inject needed:

ts
import { toast } from "@vibe-labs/design-vue-toasts";

toast("Hello world");
toast.success("Saved successfully");

Imperative API

Basic Usage

ts
// String shorthand
toast("Something happened");

// Full options
toast({
  title: "File uploaded",
  description: "report.pdf was uploaded successfully",
  variant: "success",
  duration: 3000,
});

Variant Shortcuts

ts
toast.success("Changes saved");
toast.warning("Low disk space");
toast.danger("Failed to connect");
toast.info("New version available");

// With full options
toast.success({
  title: "Order placed",
  description: "You'll receive a confirmation email shortly",
});

Promise Toast

Shows a loading toast, awaits the promise, then updates in-place to success or error:

ts
toast.promise(
  fetch("/api/data").then((r) => r.json()),
  {
    loading: "Fetching data…",
    success: "Data loaded",
    error: "Failed to load data",
  },
);

// Dynamic messages from result/error
toast.promise(saveOrder(order), {
  loading: { title: "Saving order…", description: "Please wait" },
  success: (data) => ({ title: "Order saved", description: `Order #${data.id}` }),
  error: (err) => ({ title: "Save failed", description: err.message }),
});

The promise is returned so you can chain off it:

ts
const result = await toast.promise(fetchData(), {
  loading: "Loading…",
  success: "Done",
  error: "Failed",
});

Actions

ts
toast({
  title: "Message archived",
  actions: [
    {
      label: "Undo",
      onClick: (dismiss) => {
        undoArchive();
        dismiss();
      },
    },
  ],
});

Manage Toasts

ts
// Dismiss by ID
const t = toast("Processing…");
toast.dismiss(t.id);

// Dismiss all
toast.dismissAll();

// Update in-place
const t = toast({ title: "Uploading…", duration: 0 });
toast.update(t.id, {
  title: "Upload complete",
  variant: "success",
  duration: 3000,
});

Persistent Toast

ts
toast({
  title: "Action required",
  duration: 0,     // never auto-dismiss
  closable: true,  // user can still close manually
});

Custom Render

ts
toast({
  render: ({ toast, dismiss }) => h(MyCustomToast, { toast, onClose: dismiss }),
});

Components

VibeToastContainer

Teleports to <body> and renders visible toasts. Add once at your app root.

Usage

vue
<!-- Default -->
<VibeToastContainer />

<!-- Customised -->
<VibeToastContainer position="bottom-center" :limit="3" mode="queue" :pause-on-hover="true" :default-duration="4000" />

<!-- Custom icon slot (applied to all toasts) -->
<VibeToastContainer>
  <template #icon="{ variant }">
    <SuccessIcon v-if="variant === 'success'" />
    <ErrorIcon v-if="variant === 'danger'" />
    <InfoIcon v-if="variant === 'info'" />
    <WarningIcon v-if="variant === 'warning'" />
  </template>
</VibeToastContainer>

Props

PropTypeDefaultDescription
positionToastContainerPosition"top-right"Screen position
limitnumber5Max visible toasts (stack mode)
modeToastDisplayMode"stack"stack (all visible) or queue (one at a time)
pauseOnHoverbooleantruePause auto-dismiss on hover
defaultDurationnumber5000Default auto-dismiss duration (ms)

Display Modes

ModeBehaviour
stackAll toasts visible simultaneously (up to limit), stacked vertically
queueOne toast at a time; next shows after current dismisses

VibeToast

Individual toast component. Rendered internally by the container — you do not need to use this directly.

Features: enter/exit animations with reduced-motion fallback, pause-on-hover timer that resumes where it left off, custom render function support.


Composable

useToast()

Returns the toast API and the reactive toast list. Used internally by VibeToastContainer.

ts
import { useToast } from "@vibe-labs/design-vue-toasts";

const { toast, toasts, dismiss, dismissAll, update } = useToast();

Toast Options

ts
interface ToastOptions {
  title?: string;
  description?: string;
  variant?: ToastVariant;   // success · warning · danger · info
  duration?: number;        // ms, 0 = persistent. Default 5000
  closable?: boolean;       // Default true
  actions?: ToastAction[];
  class?: string;
  id?: string;              // auto-generated if omitted
  render?: (ctx) => VNode;  // custom render function
}

Architecture

The toast system uses a singleton reactive store — a single ref<ToastInstance[]> shared across all consumers. No provide/inject needed, which means you can fire toasts from Pinia actions, API interceptors, route guards, or anywhere else outside the component tree.


Dependencies

PackagePurpose
@vibe-labs/design-components-toastsCSS tokens + generated styles

Build

bash
npm run build

Built with Vite + vite-plugin-dts. Outputs ES module with TypeScript declarations.


Usage Guide

Setup

ts
// main.ts — register CSS
import "@vibe-labs/design-components-toasts";
vue
<!-- App.vue — mount container once -->
<script setup lang="ts">
import { VibeToastContainer } from "@vibe-labs/design-vue-toasts";
</script>

<template>
  <RouterView />
  <VibeToastContainer position="top-right" />
</template>

Then call toast() from anywhere:

ts
import { toast } from "@vibe-labs/design-vue-toasts";

Imperative API — Practical Examples

CRUD Operation Feedback

vue
<script setup lang="ts">
import { toast } from "@vibe-labs/design-vue-toasts";

async function saveRecord(data: FormData) {
  try {
    await fetch("/api/records", { method: "POST", body: data });
    toast.success({ title: "Record saved", description: "Your changes have been stored." });
  } catch (err) {
    toast.danger({ title: "Save failed", description: (err as Error).message });
  }
}

async function deleteRecord(id: string) {
  await fetch(`/api/records/${id}`, { method: "DELETE" });

  toast({
    title: "Record deleted",
    variant: "warning",
    actions: [
      {
        label: "Undo",
        onClick: async (dismiss) => {
          await restoreRecord(id);
          dismiss();
          toast.success("Record restored");
        },
      },
    ],
  });
}
</script>

Promise Toast for Async Operations

vue
<script setup lang="ts">
import { toast } from "@vibe-labs/design-vue-toasts";

async function exportReport() {
  await toast.promise(
    fetch("/api/export").then((r) => r.blob()),
    {
      loading: { title: "Generating report…", description: "This may take a moment" },
      success: () => ({ title: "Report ready", description: "Your download will start shortly" }),
      error: (err) => ({ title: "Export failed", description: err.message }),
    },
  );
}
</script>

Progress Update Pattern

vue
<script setup lang="ts">
import { toast } from "@vibe-labs/design-vue-toasts";

async function syncData() {
  const t = toast({ title: "Syncing…", duration: 0 });

  for (let i = 0; i <= 100; i += 10) {
    await delay(300);
    toast.update(t.id, { title: `Syncing… ${i}%` });
  }

  toast.update(t.id, {
    title: "Sync complete",
    variant: "success",
    duration: 3000,
  });
}

function delay(ms: number) {
  return new Promise((r) => setTimeout(r, ms));
}
</script>

VibeToastContainer — Configuration Examples

Bottom Center, Queue Mode

vue
<template>
  <VibeToastContainer
    position="bottom-center"
    mode="queue"
    :default-duration="4000"
    :pause-on-hover="true"
  />
</template>

Custom Icons per Variant

vue
<template>
  <VibeToastContainer>
    <template #icon="{ variant }">
      <CheckCircleIcon v-if="variant === 'success'" class="icon-success" />
      <XCircleIcon v-else-if="variant === 'danger'" class="icon-danger" />
      <AlertIcon v-else-if="variant === 'warning'" class="icon-warning" />
      <InfoIcon v-else class="icon-info" />
    </template>
  </VibeToastContainer>
</template>

Common Patterns

Toasts from a Pinia Store (Outside Component Tree)

ts
// stores/orders.ts
import { defineStore } from "pinia";
import { toast } from "@vibe-labs/design-vue-toasts";

export const useOrderStore = defineStore("orders", {
  actions: {
    async submitOrder(order: Order) {
      try {
        const result = await api.submitOrder(order);
        toast.success({ title: "Order placed", description: `Order #${result.id}` });
        return result;
      } catch (err) {
        toast.danger("Failed to place order");
        throw err;
      }
    },
  },
});

Toasts from an Axios Interceptor

ts
// api/interceptors.ts
import axios from "axios";
import { toast } from "@vibe-labs/design-vue-toasts";

axios.interceptors.response.use(
  (response) => response,
  (error) => {
    const message = error.response?.data?.message ?? "An unexpected error occurred";
    toast.danger({ title: "Request failed", description: message });
    return Promise.reject(error);
  },
);

Persistent Confirmation Toast

ts
toast({
  title: "Publish to production?",
  description: "This will make your changes live immediately.",
  duration: 0,
  closable: false,
  actions: [
    { label: "Publish", onClick: (d) => { publish(); d(); } },
    { label: "Cancel", onClick: (d) => d() },
  ],
});

Vibe