Skip to content

Transient feedback fired from anywhere — one host at the app root, and a toast() that stacks, pauses on hover and dismisses itself.

import { BbToast } from 'bitboss-ui';

On this page

Use it for

A toast is feedback about what just happened: "Changes saved", "Upload complete", "Copied to clipboard". It appears in a corner of the screen, stacks with its siblings, and disappears on its own after four seconds.

Use something else when

  • BbAlert — the message describes the current state of the page and has to stay until someone deals with it
  • BbConfirm — the user has to decide rather than be told

Pass Through

Hover or tap a part to outline it. Toggles flip loading, errors and warnings when the component has them — only parts highlight.

Never put information a user must act on only in a toast — it is gone before they look up.

Like BbConfirm, this is a host rather than a control. <BbToast /> carries one prop, renders nothing where you put it, and everything you write goes through useToast().

One host, then a function

Mount the host once, above the router, and toast() works from anywhere after that.

vue
<template>
    <NuxtLayout>
        <NuxtPage />
    </NuxtLayout>
    <BbToast />
</template>

<script setup lang="ts">
import { BbToast } from 'bitboss-ui';
</script>

toast, update and dismiss all operate on one shared module-level stack, so every useToast() call in the app talks to the same host. With no host, messages are enqueued, never rendered, and never expire — the dismiss timer lives inside the host. With two hosts, every message renders once per host, and a dev build says so.

Fire toasts from event handlers and settled requests, never from setup during server rendering: on the server there is nothing to render them into.

This documentation site has no host of its own, so the first demo below mounts a <BbToast /> and every later demo on the page borrows it. Your application is the other way round: one host at the root, and no page mounts anything.

Communicate an outcome

toast('Changes saved.') is the whole shorthand. The object form adds a body, a tone and an icon.

A variant tints the icon and sets the announcement priority — nothing else. A success toast with no icon looks exactly like a default one, so pass an icon whenever the tone matters; dev builds warn when a built-in variant arrives without one, because otherwise the tone is dropped with no trace. warning and destructive announce assertively (role="alert"); every other variant is polite. Use the loud two sparingly, or they stop meaning anything.

duration defaults to 4000ms and must be greater than zero — toast() throws otherwise. "Never dismiss" is persistent: true, not duration: 0. The call returns the message id, which the next two sections are about.

Coming from v2

The options object was rewritten: theme is variant, timeout is duration, and showClose inverted into hideClose. Grep for timeout. It is an options key rather than a template attribute, so neither the CLI checker nor the eslint plugin sees it; TypeScript catches a literal toast({ timeout: 8000 }) on excess-property grounds, but options assembled in a variable, or any JavaScript call site, simply drop the key and take the default — which also shortened, from 6000ms to 4000ms.

Loading, then the result

Work that takes a moment gets one message, not two.

loading: true shows a spinner and pins the toast open — it implies persistent, so the countdown never starts and a swipe cannot lose it. Keep the id, and when the work settles update(id, …) morphs the same card in place and restarts the countdown. Pass loading: false in that update, or the spinner and the pinning stay.

update on an id that has already left the stack is a safe no-op, so a slow request resolving after the user cleared the corner is not a crash.

For a message that must never auto-dismiss on its own, persistent: true is the option. Add hideClose only when your own code will call dismiss(id) — the two together, with nothing to dismiss the toast, trap it on screen forever.

One toast per concern

Pass your own id and toast() becomes an upsert.

0 revisions this session

If a toast with that id is still live, it is patched in place and its countdown restarts instead of a duplicate stacking behind it. Use a stable id for anything repeatable — autosave ticks, connection status, "copied to clipboard" — so hammering the action never floods the corner.

Fields you omit keep their previous value; fields you pass overwrite, including the ones you can set back to undefined. It is the same mechanism update uses; the difference is that update never creates.

Buttons on a toast

An actions array renders buttons in a row under the text, and carries the undo pattern.

Thanks — we are on it
Your refund is on its way
Escalated to engineering

Commit the delete immediately, with no confirm dialog, and let the toast hold the escape hatch — the action's closure keeps the removed record, and if the toast expires the delete simply stands. This is the right trade when undoing is cheap and complete. When it is not, you want BbConfirm before the fact instead.

While a button's onClick runs it shows a spinner, its siblings are disabled and the countdown is held; the toast dismisses itself when the callback resolves. Two behaviours compose out of that: a rejected handler keeps the toast open and resumes the countdown, which is a retry, and a handler that calls update(id, …) takes ownership — the toast morphs in place with a fresh countdown rather than dismissing.

One or two actions is the limit. A message that needs more, or that must not be missed, is a dialog.

When the body needs more than an icon, a title, text and buttons, register your own with <BbToastPortal> and fire a toast that names it.

A plain toast cannot show a face. This one renders a registered body instead.

The portal replaces the whole body, so icon, title, text and actions stop rendering and the portal owns its own buttons. Reach for it rarely.

Where toasts appear

Six corners: top-left, top-center, top-right, bottom-left, bottom-center, bottom-right.

Each message resolves its corner in three steps: its own position, then the host's position prop, then the plugin's toastPosition (bottom-right out of the box). Messages in different corners form independent stacks. Pick one corner per product and stay there; a per-message position is for the rare control that lives somewhere else on the screen, like a copy button pinned to the top.

Everything else about the stack is automatic and worth not rebuilding: toasts pile up newest in front with only the front three visible, hovering or focusing the pile fans it out and pauses every timer, timers also pause while the browser tab is hidden and resume with the time remaining, and a toast can be swiped towards its nearest screen edge. Under 600px the stack goes full width.

The stacks are teleported into the browser's top layer, so they paint above dialogs, off-canvas panels and menus. When a modal opens, the region moves inside it — a modal makes everything outside itself inert, and a toast nobody can click is worse than no toast. When a non-modal panel claims a screen edge, the stack yields: a bottom sheet pushes bottom stacks up, a right panel pushes right stacks left, in step with the slide. Your own docked panels are ordinary page markup that the stack cannot see, so a details sidebar needs one call — useSafeArea({ side: 'right', size: 352 }) — to claim its edge the same way.

Coming from v2

placement became position, and the value vocabulary changed with it. bottom is bottom-center, bottom-start is bottom-left, bottom-end is bottom-right, and the same for top. The composable's return changed too — it is { toast, dismiss, update } now. dismissAll() is gone; dismiss() with no argument clears the stack, and nothing catches the old name for you, because it is not a template attribute either.

Variants and CSS hooks

Register the names your product needs and they join the typed union; any plain string is still accepted at the call site for a one-off.

ts
// nuxt.config.ts
export default defineNuxtConfig({
    bitboss: { toastVariants: ['upgrade'] },
});
css
.bb-toast-message.bb-toast-message--upgrade {
    --icon-color: var(--bb-primary);
}

A registered variant buys you the .bb-toast-message--<variant> class hook and nothing else — the built-ins themselves only set --icon-color, so that property is where a tone lives. Worth knowing before you theme: warning and destructive read --bb-warn and --bb-danger, while success and info are fixed colours, so retuning your palette leaves those two where they were unless you override them here. Custom variants always announce politely, whatever they are called.

The message is .bb-toast-message and the region around a stack is .bb-toast. Do not fight the stack with your own z-index, and do not mount extra hosts inside an overlay to get above it: the top layer already handles that.