Toast
Toaster is the toast provider you mount once, near the root of your app. It renders the viewports that display toasts and owns a reactive store. Render <Toaster> so it wraps (or is an ancestor of) the components that call useToast(), and the injected store is available from any descendant. createToastStore() builds a store directly when you need to share or pre-seed one.
The normal pattern is to call useToast() in a component that lives inside <Toaster>:
<!-- App.vue -->
<script setup lang="ts">
import { Toaster } from '@myghf/ui'
import SaveButton from './SaveButton.vue'
</script>
<template>
<Toaster>
<SaveButton />
</Toaster>
</template><!-- SaveButton.vue -->
<script setup lang="ts">
import { Button, useToast } from '@myghf/ui'
const toast = useToast() // resolves the store from the <Toaster> ancestor
</script>
<template>
<Button @click="toast.success('Saved', 'Your changes were saved.')">Save</Button>
</template>Toasts are added through the store, not a component prop. The store exposes add / remove / clear plus one convenience method per severity.
When the calling components cannot be nested under <Toaster> — for example, a store that must outlive a single <Toaster> or be pre-seeded — create one with createToastStore() and pass it in:
<script setup lang="ts">
import { Button, Toaster, createToastStore } from '@myghf/ui'
const toast = createToastStore({ position: 'top-end', max: 4 })
</script>
<template>
<Toaster :store="toast" />
<Button @click="toast.success('Saved', 'Your changes were saved.')">Save</Button>
</template>Examples
Using useToast() in a descendant
Wrap the component that calls useToast() in <Toaster>. The buttons below live inside the slot, so useToast() resolves the store that <Toaster> provides. (The slot content is rendered inside the provider, not as a sibling of it.)
<script setup lang="ts">
import { Toaster } from '@myghf/ui'
import UseToastButtons from './use-toast-buttons.vue'
</script>
<template>
<ClientOnly>
<Toaster>
<UseToastButtons />
</Toaster>
<template #fallback>
<span class="text-sm text-muted">Loading toasts…</span>
</template>
</ClientOnly>
</template><script setup lang="ts">
import { Button, useToast } from '@myghf/ui'
// This component is a descendant of <Toaster>, so the inject resolves.
const toast = useToast()
</script>
<template>
<div class="flex flex-wrap gap-2">
<Button variant="outline" size="sm" @click="toast.success('Saved', 'Your changes were saved.')">
Success
</Button>
<Button variant="outline" size="sm" @click="toast.warning('Unsaved changes', 'Save before leaving.')">
Warning
</Button>
<Button variant="outline" size="sm" @click="toast.danger('Upload failed', 'Check your connection and retry.')">
Danger
</Button>
</div>
</template>Severities
Each severity sets the colour and icon, and picks the live-region politeness. The demo also shows a persistent toast (duration: 0) and one with an inline action.
<script setup lang="ts">
import { Button, Toaster, createToastStore } from '@myghf/ui'
const store = createToastStore({ position: 'top-end', max: 4, duration: 5000 })
function persistent() {
store.add({ title: 'Uploading…', severity: 'info', duration: 0 })
}
function withAction() {
store.add({
title: 'Item archived',
description: 'Undo within 10 seconds.',
severity: 'warning',
action: { label: 'Undo', onClick: () => store.info('Restored', 'The item is back.') },
})
}
</script>
<template>
<ClientOnly>
<Toaster :store="store" />
<div class="flex flex-wrap gap-2">
<Button variant="outline" size="sm" @click="store.info('Heads up', 'A new result is available.')">
Info
</Button>
<Button variant="outline" size="sm" @click="store.success('Saved', 'Your changes were saved.')">
Success
</Button>
<Button variant="outline" size="sm" @click="store.warning('Unsaved changes', 'Save before leaving.')">
Warning
</Button>
<Button variant="outline" size="sm" @click="store.danger('Upload failed', 'Check your connection and retry.')">
Danger
</Button>
<Button variant="outline" size="sm" @click="store.secondary('Draft', 'Saved locally only.')">
Secondary
</Button>
<Button variant="outline" size="sm" @click="persistent()">Persistent (duration 0)</Button>
<Button variant="outline" size="sm" @click="withAction()">With action</Button>
<Button variant="ghost" size="sm" @click="store.clear()">Clear</Button>
</div>
<template #fallback>
<span class="text-sm text-muted">Loading toasts…</span>
</template>
</ClientOnly>
</template>Positions and queueing
position is logical. Set a default on the store and override it per toast; with the default max of 2 in this demo, extra toasts queue and appear as slots free up.
<script setup lang="ts">
import { Button, Toaster, createToastStore, type ToastPosition } from '@myghf/ui'
const store = createToastStore({ position: 'top-end', max: 2, duration: 6000 })
const positions: ToastPosition[] = [
'top-start',
'top-center',
'top-end',
'bottom-start',
'bottom-center',
'bottom-end',
]
function push(position: ToastPosition) {
store.add({ title: position, description: 'Per-toast position override.', position })
}
let queued = 0
function queue() {
queued += 1
store.add({
title: `Queued toast ${queued}`,
description: 'max is 2, so extras wait until a slot frees up.',
severity: 'success',
})
}
</script>
<template>
<ClientOnly>
<Toaster :store="store" />
<div class="flex flex-wrap gap-2">
<Button v-for="p in positions" :key="p" variant="outline" size="sm" @click="push(p)">
{{ p }}
</Button>
<Button size="sm" @click="queue()">Queue one (max 2)</Button>
<Button variant="ghost" size="sm" @click="store.clear()">Clear</Button>
</div>
<template #fallback>
<span class="text-sm text-muted">Loading toasts…</span>
</template>
</ClientOnly>
</template>Props
Toaster
| Prop | Type | Default | Description |
|---|---|---|---|
position | ToastPosition | 'top-end' | Default viewport corner for toasts that do not set their own position. |
max | number | 4 | Maximum toasts shown at once. Extras stay queued. Values below 1 are clamped to 1. |
duration | number | 5000 | Default auto-dismiss delay in ms. 0 means persistent. |
gap | string | '0.5rem' | CSS gap between stacked toasts in a viewport. |
label | string | 'Notifications' | Accessible label for the toast viewport region. |
store | ToastStore | — | Use an existing store instead of the one <Toaster> creates. Pass the same store you send toasts to. |
Toast options
Passed to store.add() or set through the option object behind each convenience method.
| Option | Type | Default | Description |
|---|---|---|---|
title | string | — | Bold title line. |
description | string | — | Supporting body copy. |
severity | 'info' | 'success' | 'warning' | 'danger' | 'secondary' | 'info' | Colour and icon. |
duration | number | Toaster's duration | Auto-dismiss delay in ms; 0 is persistent. |
position | ToastPosition | Toaster's position | Per-toast viewport override. |
closable | boolean | true | Shows the close button unless set to false. |
icon | string | Severity default | Lucide icon name that overrides the severity icon. |
action | { label: string; onClick: () => void } | — | Inline action button; label doubles as its accessible name. |
The default icon per severity is: info → info, success → circle-check, warning → triangle-alert, danger → circle-alert, secondary → info.
Positions are logical and mirror under RTL: top-start, top-center, top-end, bottom-start, bottom-center, bottom-end.
Events
Toaster declares no events. It displays whatever the store contains; observe the store directly if you need to react to changes.
Slots
Toaster has a default slot. Slot content renders inside the toast provider, so any descendant that calls useToast() resolves the store <Toaster> provides. Wrap your app (or the part of it that shows toasts) with <Toaster> and put the app content in the slot. The viewports are fixed-position overlays, so slot content does not affect their placement.
Sharing one queue across the app
<Toaster> creates a store and provides it to its default slot's descendants. If the components that add toasts cannot be nested under a single <Toaster> — or the queue must outlive it — create one store with createToastStore() and render <Toaster :store="store" />, then call that store's methods. useToast() is the inject shortcut for a store a <Toaster> ancestor has provided.
Exposed methods
Toaster exposes nothing through defineExpose. The API lives on the store.
useToast()
import { useToast } from '@myghf/ui'
const toast = useToast() // call in setup, at the top level
toast.success('Saved', 'Your changes were saved.')useToast() is setup-only: it calls inject(toastKey) and must run during a component's setup, not inside an event handler or after an await. Call it from a component rendered inside <Toaster> (a descendant, such as default-slot content). Without a <Toaster> ancestor it throws useToast() requires a <Toaster /> mounted above this component.
createToastStore(options?)
Builds and returns a ToastStore. options is { max?, duration?, position? }, with the same defaults as <Toaster>.
toastKey
The InjectionKey<ToastStore> used for provide/inject. Useful when you want to provide a store yourself (for tests or a custom shell) rather than through <Toaster>.
ToastStore
| Member | Type | Description |
|---|---|---|
items | Ref<ToastItem[]> | Every toast, including those queued past max. |
visible | ComputedRef<ToastItem[]> | The first max toasts still in the queue — the oldest ones — oldest first. Newer extras stay queued in items. |
add | (options: ToastOptions) => string | Adds a toast and returns its generated id. |
remove | (id: string) => void | Removes the toast with that id; a queued toast moves up. |
clear | () => void | Removes all toasts. |
info / success / warning / danger / secondary | (title: string, description?: string) => string | Convenience wrappers that add a toast at that severity. |
Accessibility
<Toaster>labels each viewport withlabel(default'Notifications'). Keep it descriptive when more than one toaster is present.- Severity drives live-region politeness:
dangerandwarningare announced assertively (reka'sforegroundtype), whileinfo,success, andsecondaryare announced politely (background). Reserve the assertive severities for messages that need immediate attention. - Reka adds a F8 shortcut that focuses the toast viewport, so keyboard users can reach toasts without a pointer.
- The close button has
aria-label="Close"; anactionpasses itslabelas the button's accessible name. Icons are decorative and marked hidden. - Auto-dismiss can outrun a screen reader. For important or actionable messages, pass
duration: 0to keep the toast until the user dismisses it, and give it adescriptionso the meaning is not carried by the title alone.
Dark mode & RTL
- Toasts use the shared
toneClassessoft treatment, which includes dark variants (dark:bg-*-900/40,dark:text-*-200) and semantic surface tokens, so both themes are covered. - Viewports are placed with logical utilities (
start-0/end-0), andtop-center/bottom-centeruseinset-x-0 mx-auto. Under RTL thestartandendviewports swap edges automatically; the centre positions stay centred. - Toast content is an inline-flex row with
gap-3andmin-w-0 flex-1; the action and close controls use logical margins, so no per-direction overrides are required.