Decide what belongs in a toast

A toast is a brief, nonblocking message about an action or change: “Preferences saved” is a good example. It should not be the only place to explain a failed form submission, request a decision, or provide information someone must read before continuing. Put those messages near the relevant content instead. This distinction makes the component simpler: a toast can appear without interrupting the task, while important content remains available until the person addresses it.

For this example, use a modern browser with JavaScript enabled and no framework. The component accepts a short text message, displays one toast at a time, and provides a Dismiss button. Messages stay visible until dismissed. That choice avoids an arbitrary reading deadline and gives both pointer and keyboard users the same control. You can add optional time limits later for low-priority messages, but they are not needed for a useful first version.

Give the queue one owner

Keep a single array of pending message strings and a reference to the currently displayed toast. An addMessage function appends a string and then calls showNext. If a toast is already visible, showNext does nothing. Otherwise, it takes the first pending message, creates the toast, and displays it. Dismissing that toast removes it, clears the current reference, and calls showNext again. This order matters: if three save operations finish close together, each result gets its turn rather than replacing the message someone is reading.

The DOM for the component needs two areas. One holds the visible toast and its Dismiss button. The other is a text-only announcement area that exists before any messages arrive. Give that second area a polite status role. When a toast becomes visible, set the announcement area’s text to its message. Keep the Dismiss button outside the status area so its label does not become part of the announcement. The visible message remains readable even if an assistive technology announcement is missed.

Treat message text as text, not markup. Assign it with textContent rather than inserting it as HTML. This prevents a caller’s message from unexpectedly creating elements or changing the component’s controls. The component can expose one small public operation, addMessage, while keeping the queue and DOM updates private.

Make dismissal predictable

Do not move keyboard focus when a toast appears. Someone may be typing in a form or navigating another part of the page, and an unsolicited focus change would interrupt them. The visible Dismiss button can still be reached through normal keyboard navigation. Give it an accessible name that describes its action; “Dismiss message” works when only one toast is shown at a time.

Consider where focus goes if the Dismiss button itself is activated. Removing a focused button without a plan can leave the keyboard position unclear. If another queued toast appears, focus its Dismiss button. If the queue is empty, move focus to a suitable, still-connected control associated with the original action when one is available. If no such control exists, avoid inventing a distant destination; design the surrounding interface so dismissal leaves a sensible next stop. Pointer dismissal does not require a forced focus move.

If you later add automatic dismissal, limit it to messages that are safe to miss. Provide enough time to read them, pause the countdown while the toast has pointer hover or keyboard focus, and let the Dismiss button work throughout. Keep the queue’s advancement tied to one removal function so a timer and a button click cannot both advance it.

Test the sequence, not just the appearance

Try adding “First saved,” “Second saved,” and “Third saved” in quick succession. Only the first should appear initially. Dismiss it and check that the second appears, followed by the third after another dismissal. Confirm that each message is announced when it becomes visible, not when it is merely added to the waiting queue. Also try adding a message after the queue becomes empty; it should display immediately.

Then use only the keyboard. Keep focus in a text field while a message arrives and verify that typing is uninterrupted. Tab to Dismiss, activate it, and check where focus lands. Finally, test a long message at a narrow viewport: its text and button should remain readable without covering essential controls. These checks exercise the component’s state, announcement timing, and interaction behavior together—where a toast queue is most likely to surprise its users.

Back to category ↗