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,maxToastsapplies 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
actioncan therefore lose it, so a message with an action that has to be reached should statetimeout: 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.