Toast

Stable v1.0.2

A toast notification system that mounts itself automatically, no provider component required. Supports loading states that resolve into success or error, action buttons, an awaitable confirm() dialog for replacing window.confirm() and one-off modals, and nested validation error lists you can click to jump straight to the broken field.

1. Interactive Playground

Configure layout variants, interaction states, and all available payload options.

Content
Duration (ms):
Appearance
Surface (Standard)
Success (Green)
Top Center
Layout
Small (360px)
Start (Left)
Modifiers & State
Live Source Code
toast.add({
  message: 'Data synchronization has resumed seamlessly.',
  title: 'Connection Restored',
  severity: 'success'
})

2. Enterprise Integration Cookbook

Ready-to-use implementations for advanced architectural patterns.

Live Scroll Targets — Employee Form
Personal Information
Employment Details
Emergency Contact

Real Validation, via @midnight-owl/use-data-validator

The form above is live — useDataValidator runs required/email/regex/custom rules against it, and its getErrorList() output is passed straight into errorList below — no hand-authored tree to keep in sync. Sections render as collapsible branches with per-group issue counts; each leaf auto-scrolls to the matching data-validate-field input. The same validator also feeds each field's own error prop, so the toast is the summary/navigator while the fields show their errors inline — click "Save Employee Record" above to try it, fix a field, and click again.

import { useDataValidator, type RulesSchema } from '@midnight-owl/use-data-validator'
import { useToast } from '@midnight-owl/ui'

// Nesting an object nests the schema -- each group becomes a collapsible
// branch in the toast's errorList below, for free.
const employeeForm = reactive({
  personalInformation: { firstName: '', lastName: '', email: '' },
  employmentDetails: { department: '', position: '', startDate: null },
  emergencyContact: { contactName: '', contactPhone: '' }
})

const employeeRules: RulesSchema = {
  personalInformation: {
    displayName: 'Personal Information',
    firstName: { required: true, displayName: 'First Name' },
    email: { required: true, email: true, displayName: 'Email Address' }
    // ... lastName
  },
  employmentDetails: { displayName: 'Employment Details', /* ... */ },
  emergencyContact: { displayName: 'Emergency Contact', /* ... */ }
}

const validator = useDataValidator()
const toast = useToast()

const saveEmployee = () => {
  if (validator.execute(employeeForm, employeeRules)) return save()

  // getErrorList() mirrors the error tree straight into the toast's
  // errorList shape -- no hand-authored array to keep in sync by hand.
  // Each leaf auto-scrolls to [data-validate-field="<path>"] on click,
  // e.g. data-validate-field="personalInformation.firstName".
  toast.danger('Click an item below to jump straight to the field.', {
    title: `Submission blocked — ${validator.errorCount.value} issues`,
    position: 'center',
    size: 'md',
    duration: 0,
    actionAlign: 'end',
    errorList: validator.getErrorList(),
    actions: [{ label: 'Review form', variant: 'solid', severity: 'danger', onClick: () => {} }]
  })
}

Awaitable confirm()

confirm() centers itself, blocks dismissal, and returns a Promise that resolves to whichever button's value the user clicks -- a direct replacement for window.confirm() and one-off modals. A component can be passed the same way as a regular toast to embed a field (e.g. capturing a name) inside the dialog before it resolves. Unsaved Changes is the same primitive used for a "save or discard?" decision instead of a destructive one -- a warning worth acting on immediately is still a decision, not a routine outcome, so it belongs here rather than as a warning() shorthand with a bolted-on action button.

// confirm() centers itself, blocks dismissal, and returns a Promise
// that resolves to the clicked button's value -- no local open/loading refs needed.
// Icon defaults to a question mark since a confirm is always "you must decide" --
// severity is still yours to pick, it only changes the color, not the icon.
const confirmed = await toast.confirm<boolean>({
    title: 'Delete employee record?',
    message:
      "This permanently deletes Chrisjelo Vega's record, including employment history and documents. This can't be undone.",
    buttons: [
      { label: 'Cancel', value: false, severity: 'secondary', variant: 'text' },
      { label: 'Delete record', value: true, severity: 'danger', variant: 'solid' }
    ]
  })
  if (confirmed) toast.success('Employee record deleted.')

// "Save or discard?" is a decision, not a routine outcome -- confirm() is the
// right primitive here, not a warning() shorthand with a bolted-on action.
const shouldSave = await toast.confirm<boolean>({
    title: 'Unsaved changes',
    message: "You have unsaved changes on this employee's record. Save before leaving?",
    severity: 'warning',
    buttons: [
      { label: 'Discard', value: false, severity: 'secondary', variant: 'text' },
      { label: 'Save now', value: true, severity: 'warning', variant: 'solid' }
    ]
  })
  toast.success(shouldSave ? 'Employee record saved.' : 'Changes discarded.')

Async Promise Resolution

Wrap your Axios or Fetch calls natively. The toast handles the persistent loading spinner and auto-transitions to success or danger upon resolution.

// Pass any native Javascript Promise
const save = axios.patch(`/api/employees/${id}`, payload)

toast.promise(save, {
  loading: 'Saving employee record...',
  success: 'Employee record saved.',
  error: 'Could not save the employee record.'
}, {
  variant: 'surface'
})

Severity Shorthands

info(), success(), warning(), and error() (an alias for danger()) all forward straight to add() with their severity pre-applied -- the routine outcomes of the same employee-record flow above, without hand-rolling add({ severity: ... }) at every call site.

// Same running Employee Records scenario, one shorthand per outcome --
// each forwards to add() with its severity already applied.
toast.info('Chrisjelo Vega\'s status changed to "On Leave".', {
  title: 'Status updated'
})

toast.success('Employee record updated successfully.')

toast.warning("Chrisjelo Vega's probationary period ends in 3 days.", {
  title: 'Review due soon'
})

// error() is an alias for danger() -- same severity, same 6s default duration
toast.error('Scheduled maintenance begins in 15 minutes. Save any unsaved changes now.', {
  title: 'Server maintenance'
})

Loading Without a Result Toast

Omit success/error from promise() and the loading toast just closes on settle -- no result toast at all. Useful when your own UI (a Modal here) is the thing that actually shows the result.

// success/error are optional -- omit both and the loading toast just
// closes on settle instead of flashing a result toast. Handy when your own
// UI (a Modal here) is what actually shows the result, not another toast.
const refreshedAt = await toast.promise(refreshEmployeeDirectory(), {
  loading: 'Refreshing employee directory...'
})

showRefreshModal.value = true

Native Component Injection

Render completely custom interactive UI -- like this inline InputText + Button note form -- inside the Toast body by passing any valid Vue Component directly into the payload. componentProps passes the employee's name in via v-bind so the form reads it as a real prop instead of closing over an outer variable.

import { ref, markRaw, h } from 'vue'
import { InputText, Button } from '@midnight-owl/ui'

const noteText = ref('')
let toastId = ''

// Wrap in markRaw so the component itself isn't made reactive. componentProps
// flows in via v-bind, so the component reads call-site data as a real prop
// instead of closing over an outer variable.
const NoteForm = markRaw({
  props: { employeeName: { type: String, required: true } },
  setup(props) {
    const addNote = () => {
      if (!noteText.value) return
      toast.remove(toastId)
      toast.success(`Note added to ${props.employeeName}'s record.`)
    }

    return () => h('div', { class: 'mt-3 flex items-center gap-2' }, [
      h(InputText, {
        modelValue: noteText.value,
        'onUpdate:modelValue': (value) => (noteText.value = value ?? ''),
        onKeyup: (e) => { if (e.key === 'Enter') addNote() },
        placeholder: 'Approved for remote work...',
        iconStart: 'lucide:pencil-line',
        size: 'sm',
        class: 'flex-1'
      }),
      h(Button, { label: 'Add', size: 'sm', onClick: addNote })
    ])
  }
})

toastId = toast.surface("Leave an HR note on this record without leaving this page.", {
  title: 'Add a note',
  component: NoteForm,
  componentProps: { employeeName: 'Chrisjelo Vega' },
  duration: 0
})

Explicit ID & Targeted Removal

Pass your own id in the payload instead of relying on the auto-generated one that add() returns -- lets a separate button (or a websocket event, a different component entirely) call toast.remove(id)/toast.update(id, ...) against a toast it never dispatched itself.

// A caller-supplied id means remove()/update() can target this exact toast
// from anywhere, without having to plumb the id that add() would otherwise
// generate and return.
const SYNC_TOAST_ID = 'directory-sync-status'

toast.info('Directory sync is running in the background.', {
  id: SYNC_TOAST_ID,
  title: 'Background sync',
  duration: 0
})

// ...later, from a websocket handler, another button, anywhere:
toast.remove(SYNC_TOAST_ID)

Toast-Level onNavigate Fallback

Every item in this errorList has canNavigate: true but no item-level onNavigate of its own (unlike the validation demo above, where useDataValidator already sets one per field) -- so every click falls through to the toast's own onNavigate callback instead of the native scroll-to-element default.

// The toast-level onNavigate is the fallback for any errorList item that
// doesn't define its own onNavigate -- every item below has none, so every
// click falls through to this single handler.
toast.danger('...', {
  title: 'Sync conflicts detected',
  position: 'center',
  errorList: [
    { key: 'employeeId', label: 'Employee ID', messages: 'Already exists.', canNavigate: true },
    { key: 'startDate', label: 'Start Date', messages: 'Conflicts with a contract.', canNavigate: true }
  ],
  onNavigate: (err) => {
    // e.g. open the relevant record, route to it, or scroll a custom container
    router.push(`/employees/conflicts/${err.key}`)
  }
})

Rich HTML Parsing

Pass the html: true flag to evaluate the message (and title) string natively -- a popup-ad-style promo banner with a gradient block, a badge, and a CTA link, not just plain text.

toast.info(`
    Unlock automated payroll runs, tax filing, and audit trails for your HR team.
    <div class="mt-2 overflow-hidden rounded-md border border-main/10">
      <div class="bg-gradient-to-br from-primary to-secondary p-2.5 text-white">
        <div class="... uppercase opacity-80">Limited-time offer</div>
        <div class="mt-0.5 font-bold">30% off Payroll Pro this week</div>
      </div>
    </div>
    <a href="#" class="mt-2 inline-block text-primary hover:underline">Claim the offer →</a>
  `, {
  title: 'Special offer <span class="...">New</span>',
  html: true,
  icon: 'lucide:megaphone'
})

API Reference

Complete technical specifications for the Composable Methods and Payload options.

useToast() Methods

Property / Method
Type / Signature
Description
add(options)
(opts: ToastOptions) => string
The core dispatcher. Accepts the full options payload. Caps each position at 8 concurrent toasts, evicting the oldest -- but never a persistent one (duration: 0, e.g. loading()/promise() mid-flight, or an unresolved confirm()), since silently killing those would drop a pending result or leave a confirm() Promise hanging forever. Persistent toasts get their own much higher ceiling (20 per position) purely as a safety net against a trigger spamming loading()/confirm() with no re-entrancy guard -- exceeding it logs a dev-only console warning pointing at that as the likely cause.
success | error | info ...
(msg: string, opts?: Options) => string
Helper wrappers that automatically apply the severity.
loading(msg, opts)
(msg: string, opts?: Options) => string
Dispatches a persistent toast with a spinner.
promise(promise, msgs, opts)
(promise, { loading, success?, error? }, opts) => Promise
Wraps a Promise, automatically updating the toast on resolve/reject. Omit success/error to just close the loading toast on settle instead -- useful when the caller hands off to its own UI (e.g. opening a Modal) rather than showing a result toast.
confirm(options)
<T>(opts: ToastConfirmOptions) => Promise<T>
Awaitable confirmation dialog. Centers itself, blocks dismissal, and resolves to the value of whichever button was clicked. Defaults to variant "solid", overlay: true, and a question-mark icon -- severity is still yours to pick and only changes the color, not the icon. Pass icon explicitly to override it.
update(id, newOptions)
(id: string, opts: Partial<Options>) => void
Transitions an existing toast dynamically (e.g., Loading -> Success).
remove(id)
(id: string) => void
Programmatically dismisses a specific toast.
clearAll()
() => void
Instantly sweeps all active toasts from the screen.

ToastOptions Payload

Property / Method
Type / Signature
Description
message
string
The primary text content.
title
string
Optional bold header above the message.
severity
"primary" | "secondary" | "success" | "warning" | "danger" | "info" | "surface"
Semantic color intent. Defaults to "surface" for add(); the severity shorthands (success(), danger(), ...) apply theirs automatically. Also drives the default icon and, via the severity-keyed duration map, how long the toast stays onscreen.
variant
"surface" | "solid" | "soft" | "outline"
The visual rendering style of the card. confirm() defaults this to "solid".
icon
string
Overrides the severity's default icon (a lucide: name). confirm() defaults this to a question mark ("lucide:circle-help") instead of a severity-based icon -- severity still picks the color, it just no longer picks the icon for a confirm dialog.
size
"xs" | "sm" | "md" | "lg" | "xl"
Expands maximum width of the toast and internal scroll height of lists.
position
"top-right" | "top-left" | "bottom-right" | "bottom-left" | "top-center" | "bottom-center" | "center"
Defaults to "top-center" for regular notifications (success/error/promise results) and "center" for confirm() -- keep it that way app-wide so ambient notifications land front-and-top-center while a confirmation you must act on interrupts dead center. Only override for a deliberately distinct, large blocking surface (e.g. a full validation-error dialog).
errorList
ToastErrorItem | ToastErrorItem[] | Record<string, any>
Structured validation errors formatted into interactive recursive trees.
onNavigate
(err: ToastErrorItem) => void
Fallback callback fired when a user clicks an item lacking a specific onNavigate function.
id
string
Explicit id for this toast, instead of the auto-generated `toast-<timestamp>-<n>`. Pass your own to target it later with update(id, ...) or remove(id) without having to capture the id returned from add().
component
Component
A raw Vue component to natively inject into the Toast body.
componentProps
Record<string, any>
Props passed straight through to `component` via v-bind, for a component that needs data supplied from the call site rather than closing over it.
duration
number
Time in ms before auto-dismissal. 0 sets it to persistent. When omitted, defaults per severity (4s for primary/secondary/success/info/surface, 5s for warning, 6s for danger) so messages with more to read or more consequence for missing them stay onscreen longer -- an explicit value here always wins over that default. A persistent (0) toast is also exempt from the per-position toast cap -- see add() above.
closable
boolean
If false, hides the close icon and disables swipe-to-dismiss.
overlay
boolean
If true, renders a secure backdrop that blocks page clicks. confirm() defaults this to true.
actions
ToastAction[]
Array of interactive buttons rendered below the text.
actionAlign
"start" | "center" | "end"
Controls the flex-box horizontal alignment of the action buttons.
loading
boolean
If true, shows a spinner and freezes the duration timer.
html
boolean
If true, safely parses the message content via v-html.

ToastConfirmButton (confirm() buttons[])

confirm(options) takes the same ToastOptions payload above (minus actions/onNavigate) plus a required buttons array:

Property / Method
Type / Signature
Description
label
string
Text rendered on the button.
value
T
The confirm() promise's resolved value when this button is clicked. Defaults to label.
severity
"primary" | "secondary" | "success" | "warning" | "danger" | "info" | "surface"
Button color.
size
"xs" | "sm" | "md" | "lg" | "xl"
Button size.
variant
"solid" | "outline" | "text" | "link"
Button visual weight.

ToastErrorItem Interface

Property / Method
Type / Signature
Description
key
string
Optional unique identifier for the field (used for scrolling).
label
string
Display name of the field or error group.
messages
string | string[]
The error message(s) associated with this item.
canNavigate
boolean
If true, shows the go-to icon on the row, which smooth-scrolls to the element matching key.
onNavigate
() => void
Executes specific code (like opening a modal) when clicked.
children
ToastErrorItem[]
Deeply nested recursive array for grouped validation errors.