Skip to contentAsheeUIAsheeUI

Toast

A short report of what just happened, drawn above the page and gone on its own.

A toast answers one question, did the thing I pressed work?, and then gets out of the way. It is drawn above the page rather than in it, because the reader's attention is on the control they pressed, not on a corner of the form; it dismisses itself, because the answer is only interesting for a few seconds.

It is the one component on these pages you do not place. There is a provider that holds the queue and a hook that fills it, and nothing to render where the message will appear. A toast is also a status, never an alert: a result is reported, not shouted at.

Usage

import { Button, ToastProvider, useToast } from "@asheeui/web";

function App() {
  return (
    <ToastProvider>
      <SaveInvoice />
    </ToastProvider>
  );
}

function SaveInvoice() {
  const toast = useToast();

  return (
    <Button onClick={() => toast.success("Invoice saved")}>Save invoice</Button>
  );
}

toast.success(message, options) is the shorthand; toast.error, toast.info and toast.warning are its siblings. toast(options) takes the whole message at once, its type, title, message, action and the overrides below, and every one of them returns the message's id, which is what removeToast takes.

Placing the provider

Put ToastProvider once, at the root of the application, above the routes and inside the application's AsheeUIProvider, because it reads the resolved configuration.

Where it sits in the tree does not change where a message appears: the queue is rendered into a portal at the document body by default, which is how it escapes a scrolling panel's overflow and a stacking context's z-index. What the position decides is who can report, everything that calls useToast() has to be beneath it.

Two placements are mistakes:

  • Inside a route. A provider that unmounts on navigation takes its queue with it, so a message fired just before the reader moved on disappears with the page that fired it.
  • A second, nested provider. It starts a queue of its own, and every useToast() beneath it reports to the inner provider. Both queues draw into the same corner of the window, maxToasts applies twice, and the order of messages stops being one order. Nothing errors, which is exactly why it hides in review.

The hook degrades rather than throwing: outside a provider, useToast() returns no-op functions and an empty toasts array. A missing provider is therefore a button that does nothing, not a crash, so when a message does not appear, check the tree before the message. In development the first such call also warns once, naming the likely mistakes (a missing provider, or a nested one), because a button that silently does nothing is the hardest kind of bug to see and a warning on every render would be the second.

Message types

The type decides the glyph and the colour role: success is success, error is danger, warning is warning, info is primary, and the general default type is secondary for a message that is neither praise nor blame. Every one of them is polite. A failure that has to interrupt, or that describes the state of something on the page rather than the result of a press, is an Alert at the error or warning intent.

The queue and its timer

A message waits 3500ms by default. timeout states its own wait; 0 keeps it until the reader dismisses it. The timer starts when the message appears and pauses while the pointer is over it, resuming with the time that was left rather than a fresh budget: a message that vanished while it was being read, or while a control inside it was being reached for, would punish the reader for paying attention.

maxToasts is 5. A new message arrives at the top of its stack and the oldest one beyond the limit is dropped, because a queue deeper than a reader can read is a queue that hides messages. Passing an id that is already in the queue replaces that message instead of adding a second one, so a progress message can update in place.

placement, size, variant, radius and animated are stated on the provider and can be overridden per message, a message that belongs to a panel at the bottom of the screen does not have to arrive in the same corner as the rest. dismissible={false} removes the close control; combined with timeout: 0 it produces a message the reader cannot get rid of, which is a decision to take deliberately. action is a node rendered under the message, for the one thing the reader may want to do next.

The hook exposes the queue as well as filling it: toasts is the current array, removeToast(id) drops one, and clearToasts() empties it.

Props

size (md), placement (top-right), radius (md), animated (true), defaultTimeout (3500), maxToasts (5) and portal (true) are all registered under components.toast, so a configuration file can state the queue's habits once and every provider inherits them; a prop on the provider overrides the registered value, and an option on a single message overrides the provider.

Two of the options behave differently from their siblings. variant is not registered, so a toast follows the application's defaultVariant the way an alert does: the shipped configuration makes that faded, and an application that sets solid gets solid toasts without saying so again, state components.toast.variant, or the message's own variant, to pin one. radius, by contrast, is registered, so the application's defaultRadius never reaches a toast; state components.toast.radius to round the queue. portal is a web option: the platform has no document to portal into.

Accessibility

  • Every message is role="status", polite, so it is announced when the reader is idle rather than over the top of a sentence, and each message lives inside the stack's <section>, a named region a reader can also jump to. The name carries the placement, Notifications (top right), Notifications (bottom centre), because two stacks on one screen would otherwise offer the same name twice and say nothing about which corner each one is in.
  • Nothing takes focus, and nothing should: a toast reports on an action without moving the reader away from where they acted. The dismiss control is a real button, named Dismiss notification, and sits in the tab order after the page's own content.
  • The clock pauses for the pointer, not for focus. A keyboard reader tabbing towards a message's action can therefore lose it, so a message with an action that has to be reached should state timeout: 0.
  • A toast says what the screen does not: the result of something the reader pressed. A message that repeats what is already visible is noise that interrupts the next thing.

Which surface announces which situation, and why some of them are deliberately silent, is the subject of Announcements.