Skip to content

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>:

vue
<!-- App.vue -->
<script setup lang="ts">
import { Toaster } from '@myghf/ui'
import SaveButton from './SaveButton.vue'
</script>

<template>
  <Toaster>
    <SaveButton />
  </Toaster>
</template>
vue
<!-- 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:

vue
<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.)

vue
<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>
vue
<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.

vue
<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.

vue
<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 ​

PropTypeDefaultDescription
positionToastPosition'top-end'Default viewport corner for toasts that do not set their own position.
maxnumber4Maximum toasts shown at once. Extras stay queued. Values below 1 are clamped to 1.
durationnumber5000Default auto-dismiss delay in ms. 0 means persistent.
gapstring'0.5rem'CSS gap between stacked toasts in a viewport.
labelstring'Notifications'Accessible label for the toast viewport region.
storeToastStore—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.

OptionTypeDefaultDescription
titlestring—Bold title line.
descriptionstring—Supporting body copy.
severity'info' | 'success' | 'warning' | 'danger' | 'secondary''info'Colour and icon.
durationnumberToaster's durationAuto-dismiss delay in ms; 0 is persistent.
positionToastPositionToaster's positionPer-toast viewport override.
closablebooleantrueShows the close button unless set to false.
iconstringSeverity defaultLucide 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() ​

ts
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 ​

MemberTypeDescription
itemsRef<ToastItem[]>Every toast, including those queued past max.
visibleComputedRef<ToastItem[]>The first max toasts still in the queue — the oldest ones — oldest first. Newer extras stay queued in items.
add(options: ToastOptions) => stringAdds a toast and returns its generated id.
remove(id: string) => voidRemoves the toast with that id; a queued toast moves up.
clear() => voidRemoves all toasts.
info / success / warning / danger / secondary(title: string, description?: string) => stringConvenience wrappers that add a toast at that severity.

Accessibility ​

  • <Toaster> labels each viewport with label (default 'Notifications'). Keep it descriptive when more than one toaster is present.
  • Severity drives live-region politeness: danger and warning are announced assertively (reka's foreground type), while info, success, and secondary are 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"; an action passes its label as 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: 0 to keep the toast until the user dismisses it, and give it a description so the meaning is not carried by the title alone.

Dark mode & RTL ​

  • Toasts use the shared toneClasses soft 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), and top-center / bottom-center use inset-x-0 mx-auto. Under RTL the start and end viewports swap edges automatically; the centre positions stay centred.
  • Toast content is an inline-flex row with gap-3 and min-w-0 flex-1; the action and close controls use logical margins, so no per-direction overrides are required.

Released under the MIT License.