Skip to main content

Notes · Interface craft

Building a Toast Component

States, stacking, motion, accessibility, and API shape as a small proof of design engineering judgement.

A toast looks like a tiny component until it has to behave in public. Then the small decisions pile up fast: what interrupts the user, what disappears on its own, what waits in a queue, what a screen reader should hear, and whether motion explains state or just makes the interface feel busy.

I like toast components because they expose a team’s taste for interaction. A sloppy one teaches users to ignore feedback. A careful one stays out of the way until it has something useful to say.

Interactive demo

Toast states under a little pressure.

Queue a few messages, dismiss one manually, then watch the stack keep its shape. The demo uses plain browser JavaScript so the behaviour is visible without a framework island.

Queued for review Waiting for the latest spec.
Prototype published Ready for design review.

The State Model

The useful states are less about colour than intent.

  • Default confirms a lightweight action without demanding attention.
  • Success tells the user a requested thing happened and can usually dismiss itself.
  • Warning carries risk, so it stays visible longer and gets an assertive announcement.
  • Loading should not pretend the work is done. It waits, then either resolves or gets replaced.

That last part matters. Loading toasts are often decorative spinners pretending to be status. If the operation can fail, the API should make resolution explicit.

The public API stays small on purpose.
type ToastVariant = 'default' | 'success' | 'warning' | 'loading'

type ToastInput = {
  title: string
  description?: string
  variant?: ToastVariant
  timeout?: number
}

toast.queue({
  title: 'Prototype published',
  description: 'Ready for design review.',
  variant: 'success',
  timeout: 4600
})

Stacking Is an Editorial Decision

The stack should feel composed, even when the product is noisy. New messages arrive at the top because they are the freshest information. Older messages move down and eventually leave. Four visible items is enough for the demo; more than that starts to look like the product has lost control of itself.

Manual dismissal is not optional. Timeouts are helpful for low-risk feedback, but the user should always be able to clear the surface. A warning gets more time because it may need to be read twice. Loading stays until something replaces it or the user clears it.

Motion Has One Job

Motion explains where the toast came from and where it went. That is all it needs to do.

The entrance is short and vertical, matching the direction of the stack. The exit is quicker because removal should feel decisive. Users who prefer reduced motion get the same information without animated movement. The demo also avoids layout tricks that would make the stack depend on animation timing.

Accessibility Changes the API

Accessibility is not a layer on top of a toast. It changes the design of the component.

Status messages use polite announcements. Warnings use assertive announcements because they may affect the next action. Dismiss buttons have labels that include the toast title, because a stack of buttons all called “Dismiss” is lazy for assistive technology. Focus states stay visible. The component does not steal focus when a toast appears, because passive feedback should not hijack a user’s task.

The constraint is simple: if the toast matters enough to show, it matters enough to announce correctly.

What I Would Ship Next

In production, this would move behind a small store with queue, update, and dismiss methods. Loading toasts would get stable IDs so async operations can resolve the same notification instead of adding a second one. I would also add viewport-aware placement rules, because bottom-right is tidy on desktop and often hostile on mobile.

For this note, the point is narrower. The component proves the behaviour: queued states, manual dismissal, automatic timeout, reduced-motion handling, and enough API shape to show how the implementation would grow without turning into documentation soup.