Toaster

Messages in the browser's top layer with no portal: one live region that exists before there is anything in it, a queue rather than a cap, and timers that stop on hover, on focus and in a background tab.
Import
JSX
import Toaster, { toast } from '@box-kite/react/components/toaster';
Usage
JSX
// once, near the root of the app
<Toaster />

// then from anywhere at all
toast.success('Saved');

It is called, not rendered

toast() writes to a store that has no React in it, so a message can come from an event handler, a fetch, a router guard or a module that has never heard of a component. <Toaster /> only draws what is there — which also means a call made before the viewport mounts is queued rather than lost.
JSX
const id = toast.loading('Uploading…');

await upload(file);
toast.update(id, 'Uploaded', { kind: 'success' });
toast.promise is that pair written once, and it hands the promise back untouched so it can still be awaited.
JSX
toast.promise(save(), {
  loading: 'Saving…',
  success: (saved: { name: string }) => `Saved as ${saved.name}`,
  error: 'Could not save',
});

A description, and one thing to do about it

A toast takes a description under its message and a single action. The action dismisses the toast it answered unless it says otherwise — an undo that left its own toast standing would invite a second press.
JSX
toast('Row deleted', {
  description: 'Ada Lovelace, added in March.',
  action: { label: 'Undo', onClick: restore },
});

The limit is a queue, not a cap

Past limit a toast waits its turn with its timer unstarted, so nothing expires that was never on screen — and the counter at the far end of the stack says how many are still to come. A cap would simply throw the fifth message away.
JSX
<Toaster limit={3} duration={5000} />

Six corners, and the inline half is logical

start and end rather than left and right, so a right-to-left page moves the stack to the other side with nothing declared twice. The newest toast is always the one nearest the screen edge, which is why a stack pinned to the top is drawn in the opposite order to one pinned to the bottom.
JSX
<Toaster position="top-center" />

Four things stop the clock

Hover, focus and a background tab
A timer stops while the pointer is over the stack, while anything in it has focus, and while the tab is not being looked at — and picks up where it left off rather than starting again. WCAG 2.2.1 is what makes a toast allowed to carry a control at all: a button that can vanish mid-reach is not operable.
A dismissal is the fourth
onDismiss is told which of timeout, close, action or imperative it was, the way every other component in the library reports a change.
JSX
toast('Held', { duration: Infinity, onDismiss: (reason) => log(reason) })

The top layer, and no portal

The viewport carries popover="manual" and is shown the moment it mounts, so it paints over every stacking context and outside every clipped or transformed ancestor — and because it never leaves the place it was declared, it inherits the theme, the custom properties and the text direction around it. manual rather than auto because a stack of messages owns no dismissal: a press outside has to reach the page.
Which raises the question a fixed strip across a corner always raises — the viewport would swallow every click in it. It takes no pointer events at all, and the toasts take them back, so a press in the gaps goes through to whatever is underneath.

Nothing is stolen, and everything is reachable

F6 gets you there
A toast never takes focus when it arrives — an announcement is not an interruption. F6 moves focus to the stack from anywhere on the page, Tab walks the toasts in the order they are on screen, and Escape dismisses the one focus is in and hands focus back where it came from once there is nothing left to read. hotkey takes a combination ("alt+t") or false.
One region, and it was already there
The viewport is a polite live region from the moment it mounts, before there is anything in it — a region inserted together with its content is not reliably announced. An error toast is role="alert", which is assertive and is the one announcement pattern every screen reader implements; nothing else carries a region of its own, because the nearest one to a change is the one that speaks.

Styling

Every part is a Box component key: toaster for the viewport, then toaster.toast, toaster.message, toaster.description, toaster.action, toaster.close and toaster.overflow. The kind is a variant on the toast, and the accent it paints is never the only signal — the message itself is what says what happened.
JSX
Box.components({
  toaster: {
    children: {
      toast: { styles: { borderRadius: 4, shadow: 'large' } },
    },
  },
})

Toaster props

Everything below is this component’s own. All 235 of Box’s style props work on it too, and those are on /box rather than repeated here.
PropTypeDefaultWhat it does
positionToastPosition'bottom-end'Which corner the stack is pinned to. Default 'bottom-end'.
limitnumber3How many toasts are on screen at once. The rest wait their turn, timers and all. Default 3.
durationnumber5000Milliseconds a toast that names no duration of its own stays. Default 5000.
labelstring'Notifications'The region's accessible name. role="region" has none of its own, and this one is never labelled visually.
closeLabelstring'Close'The close button's accessible name.
overflowLabel?(count: number): React.ReactNode;(count: number) => `+${count} more`What the counter at the end of the stack says when the limit is holding toasts back.
hotkeystring | false'F6'A key that moves focus to the stack from anywhere on the page, since toasts steal none. Default 'F6'; 'alt+t' and the like also work, and false turns it off.
storeToastStore<Content>defaultToastStoreA store of your own — for a second, independent stack, or for a test that must not share state.

Toaster keyboard

KeyResult
F6Moves focus to the stack from anywhere on the page. Configurable through hotkey.
TabThrough the toasts and their controls, in the order they are on screen.
EscapeDismisses the toast focus is in, and hands focus back to where it came from once the stack is empty.

Toaster accessibility

  • The viewport is a polite live region that exists from the moment it mounts, **before there is anything in it** — an aria-live element inserted together with its content is not reliably announced.
  • An error toast is role="alert", which is assertive; every other kind is announced politely by the region around it. A toast is never focused on arrival, so nothing is taken away from the keyboard.
  • Timers stop while the pointer is over the stack, while anything in it has focus, and while the tab is in the background — WCAG 2.2.1, and the reason a toast may carry a control at all.
  • The accent bar says the kind a second time; it is never the only signal, since the message itself is what says what happened.
Swept with axe on every release, in this state: Toaster. No violations, with contrast and landmark rules left to a human. Screen-reader results are not published yet.

Toaster style tree

Every part the component draws is a node with a name, so a default can be restyled with Box.components() instead of a selector — and a variant is a name too.
toastervariants: topStart, topEnd, topCenter, bottomStart, bottomEnd, bottomCenter
toaster.toastvariants: fromTop, success, error, warning, info, loading
toaster.message
toaster.description
toaster.action
toaster.close
toaster.overflow