Appearance
@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
| Prop | Type | Default | Description |
|---|---|---|---|
position | ToastContainerPosition | "top-right" | Screen position |
limit | number | 5 | Max visible toasts (stack mode) |
mode | ToastDisplayMode | "stack" | stack (all visible) or queue (one at a time) |
pauseOnHover | boolean | true | Pause auto-dismiss on hover |
defaultDuration | number | 5000 | Default auto-dismiss duration (ms) |
Display Modes
| Mode | Behaviour |
|---|---|
stack | All toasts visible simultaneously (up to limit), stacked vertically |
queue | One 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
| Package | Purpose |
|---|---|
@vibe-labs/design-components-toasts | CSS tokens + generated styles |
Build
bash
npm run buildBuilt 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() },
],
});