Skip to content

Toast

Transient confirmation that something happened. It disappears on its own, which means it must never carry the only copy of anything important.

Also called Notification, Snackbar, Flash Message, Growl — in this system all of them are Toast.

Live preview

Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.

Playground

Appears bottom-right. Duration scales with the length of the text and caps at nine seconds.

Tones

Four tones, same structure. The danger toast is the only one that uses role="alert" and interrupts a screen reader.

Deployment queued

api-gateway will be live in about 40 seconds.

Copied to clipboard

Rate limit approaching

92% of your hourly quota used.

Undo instead of confirm

Deleting immediately with an Undo toast is faster than a confirmation dialog and recovers just as well. The dialog interrupts everyone to protect against a rare mistake.

api-gateway
billing-worker
edge-cache

Stacking and duration

Newest nearest the corner, capped at four. Fire several quickly to see the cap — and note that longer messages stay longer.

Interaction states

Every state a user can put this component into, rendered side by side. If a state is missing here, it is missing in production too.

Saved

Success
Dangerrole="alert"

Copied

Info

Quota at 92%

Warning

Deleted

With action

Queued

Live in ~40s.

Two-line
PersistentNo auto-dismiss
translateY 8px → 0
Enteringslide-up 220ms

First

Second

Stacked
max 4 visible
Capped
4s + reading
Duration
fade only
Reduced motion

Anatomy

Every part, every measurement, and the reason it is that number.

Icon, title, one line of description, one action, one dismiss. Anything more than this belongs somewhere permanent.

  1. Width384px, capped to the viewport

    About 55 characters per line — narrow enough to read in one glance, wide enough for a real sentence.

  2. Surface--ds-surface-overlay + e4

    Overlay-level elevation, because it floats above everything including dialogs’ scrims in some flows.

  3. PositionBottom-right, 20px inset

    Out of the reading path and away from the primary action. Top-centre collides with OS notifications.

  4. Duration4s + chars ÷ 18, max 9s

    Computed from the text length. A fixed three seconds is unreadable for anything longer than one word.

  5. ActionExactly one, text style

    A second action turns an announcement into a decision, and decisions must not disappear on a timer.

  6. Entranceslide-up 220ms emphasized

    8px of travel plus a fade. Enough to catch peripheral vision without pulling the eye off the task.

Design tokens used

Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.

Color

TokenValueUsed for
--ds-surface-overlay—Toast background
--ds-border—Edge definition
--ds-success-text—Tone icon
--ds-accent-text—Action label
--ds-fg-secondary—Description

Spacing

TokenValueUsed for
paddingAll sides
stack gapBetween stacked toasts
viewport insetDistance from the corner

Radius

TokenValueUsed for
--radius-lgCorners

Shadow

TokenValueUsed for
--shadow-e4—Elevation

Motion

TokenValueUsed for
slide-upEntrance

Recommended sizes

Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.

SizeHeightPaddingLabel gapMax widthWhen to use
Single line52px14px—384px“Copied to clipboard.” No description, no action.
Two line76px14px—384pxTitle plus one line of detail. The common case.
With action104px14px—384pxAdds an Undo or a View link below the description.
Stack——10px384pxFour visible maximum. Older toasts are dropped, not queued.
Mobile———calc(100vw − 40px)Full width minus the gutters, anchored bottom above the safe area.

Do

4000 + (chars / 18) * 1000capped at 9000ms
Scale the duration to the textFour seconds plus reading time. A fixed three seconds means a two-line message is gone before it is finished, and users learn to ignore toasts entirely.
api-gateway
billing-worker
edge-cache
Offer Undo instead of a confirmation dialogA dialog interrupts everyone to protect against a mistake most people never make. Undo lets the common case be fast and still makes the rare case recoverable.
Make anything copyable persistentAn error ID or a support reference disappearing after six seconds is a guaranteed support ticket. Persistent toasts wait for a deliberate dismiss.
onMouseEnter={pause} onFocusCapture={pause}
Pause the timer on hover and focusA user who has moved the pointer onto the toast is reading it. Continuing to count down is actively hostile.

Don't

Do not put critical information in a toastIt disappears. If the user looked away, the information is gone with no way to retrieve it — and there is no history to scroll back through.

Row 1 saved

Row 2 saved

Row 3 saved

Row 4 saved

Row 5 saved

Do not queue a backlogTwelve toasts appearing one after another for eleven seconds is a log file with animation. Cap the stack and collapse repeats into a count.
new-service · just added

Row added

Do not toast something already visibleIf the row appeared in the table, the user saw it. A toast saying "Row added" is redundant motion competing with the change it is describing.

Reassign this task?

Do not put a form or two actions in a toastA decision cannot have a timeout. Anything requiring more than one tap is a dialog or an inline surface, not something that vanishes.

Accessibility

Not a checklist to run at the end. These are the requirements the component was built from.

2.2.1Timing AdjustableA4.1.3Status MessagesAA1.4.13Content on Hover or FocusAA2.1.1KeyboardA

Contrast

  • The overlay surface is lighter than the page in dark mode and white with a shadow in light mode, so both need their own contrast check against the toast text.
  • The action link must reach 4.5:1 against the toast background and must not rely on colour alone — ours underlines on hover and focus.

Keyboard

TabReaches the action and the dismiss button once a toast is present.
EscDismisses the most recent toast when focus is inside the toast region.
F6Some screen readers use this to jump to the notification region. Keeping the region labelled makes that work.

Screen readers

  • The live region must be in the DOM on page load. Mounting the container and the toast together means many screen readers announce nothing.
  • Keep the announcement short — the title alone is usually enough. Screen-reader users cannot skim a toast the way sighted users can.
  • WCAG 2.2.1 means auto-dismiss must be adjustable or avoidable. Pausing on hover and focus, plus a persistent option, satisfies it.

Focus & touch

  • A toast must never steal focus. It appears, it is announced, and the user carries on typing. Focus only moves into it if the user tabs there deliberately.
  • On mobile, anchor to the bottom above the safe-area inset and make the whole toast swipeable to dismiss. Keep it clear of any bottom navigation — a toast covering the tab bar is a trap.
AttributeApplied toNotes
role="region" + aria-labelThe toast container"Notifications". Lets a screen-reader user navigate back to it deliberately.
role="status"Non-error toastsAnnounced at the next pause. The container must exist before the toast is added.
role="alert"Danger toastsInterrupts. Reserved for failures the user needs to know about immediately.
aria-liveThe containerpolite or assertive to match. Setting it on the toast itself is too late — the region has to pre-exist.
aria-labelThe dismiss button"Dismiss notification". Never a bare ×.

Code

Example usage

tsx
1import { ToastProvider, useToast } from '@/ui/Feedback'23// Mount the provider once, high in the tree. The live region must4// exist before any toast is added, or nothing is announced.5<ToastProvider position="bottom-right" max={4}>6  <App />7</ToastProvider>89// Anywhere below it10const { toast, dismiss } = useToast()1112toast({ tone: 'success', title: 'Deployment queued',13        description: 'api-gateway will be live in about 40 seconds.' })1415// Undo instead of a confirmation dialog16async function deleteProject(p: Project) {17  const snapshot = p18  remove(p.id)                       // optimistic19  const id = toast({20    tone: 'neutral',21    title: p.name + ' deleted',22    action: { label: 'Undo', onClick: () => restore(snapshot) },23  })24  try {25    await api.delete(p.id)26  } catch {27    restore(snapshot)28    dismiss(id)29    toast({ tone: 'danger', title: 'Could not delete ' + p.name })30  }31}3233// Persistent: anything the user may need to copy34toast({35  tone: 'danger',36  title: 'Deployment failed',37  description: 'Error 4f21c-8821',38  persistent: true,39})

Framework-free HTML

html
<!-- The region is rendered on page load and stays empty -->
<div class="ds-toasts" role="region" aria-label="Notifications">
  <div class="ds-toast" role="status" aria-live="polite">
    <svg class="ds-toast__icon" aria-hidden="true">…</svg>
    <div class="ds-toast__content">
      <p class="ds-toast__title">Deployment queued</p>
      <p class="ds-toast__desc">api-gateway will be live in about 40 seconds.</p>
      <button class="ds-toast__action">Undo</button>
    </div>
    <button class="ds-toast__dismiss" aria-label="Dismiss notification">
      <svg aria-hidden="true">…</svg>
    </button>
  </div>
</div>

CSS

css
.ds-toasts {
  position: fixed;
  inset-block-end: 0;
  inset-inline-end: 0;
  z-index: 90;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 20px;
  pointer-events: none;              /* the stack never blocks the page */
}
.ds-toast { pointer-events: auto; }

.ds-toast {
  inline-size: min(24rem, calc(100vw - 2.5rem));
  display: flex;
  gap: 12px;
  padding: 14px;
  background: var(--ds-surface-overlay);
  border: 1px solid var(--ds-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-e4);
  animation: slide-up 220ms var(--ease-emphasized) both;
}

@keyframes slide-up {
  from { opacity: 0; transform: translateY(8px) }
  to   { opacity: 1; transform: translateY(0) }
}

/* Mobile: full width above the safe area, clear of any bottom nav */
@media (max-width: 640px) {
  .ds-toasts {
    inset-inline: 0;
    padding: 12px;
    padding-block-end: max(12px, env(safe-area-inset-bottom));
  }
}

@media (prefers-reduced-motion: reduce) {
  .ds-toast { animation-name: fade-in; }
}

Component API

useToast()

PropTypeDefaultDescription
toast(item)(t: Omit<ToastItem,"id">) => string—Shows a toast and returns its id.
dismiss(id)(id: string) => void—Removes a toast early — useful when an optimistic action resolves.
itemsToastItem[]—The current stack. Rarely needed outside the provider.

ToastItem

PropTypeDefaultDescription
title*string—One short line. This is what gets announced.
descriptionstring—One line of detail. Two lines maximum.
toneTone'neutral'danger switches the role to "alert".
action{ label, onClick }—Exactly one. Dismisses the toast after firing.
durationnumber—Override the computed duration. Rarely correct.
persistentbooleanfalseNo auto-dismiss. Use for anything copyable.

ToastProvider

PropTypeDefaultDescription
position'bottom-right' | 'bottom-center' | 'top-right' | 'top-center''bottom-right'Bottom-right on desktop, bottom-center on mobile.
maxnumber4Older toasts are dropped rather than queued.

Notes

Professional tips

  • Collapse repeats: "3 rows deleted" instead of three separate toasts. Group by action type within a short window.
  • Undo should remain available for the full toast duration, and the action must be genuinely reversible — a fake Undo that fails is worse than no Undo.
  • On mobile, support swipe-to-dismiss. It is the gesture users already expect and it removes the need for a small × target.
  • If a background job takes minutes, do not toast at the start and the end. Toast at the end only, or use a persistent progress surface.

Performance

  • Render toasts in a portal at the body. Inside a transformed ancestor, position: fixed silently stops being fixed.
  • Clear timers on unmount. A dangling setTimeout that calls setState after the provider unmounts is a classic React warning and a real leak.
  • Cap the array rather than queueing. An unbounded queue during a burst of events keeps animating long after the burst is over.
  • Animate transform and opacity only. A stack of four toasts animating layout properties drops frames on low-end devices.

Common mistakes

  • Mounting the live region at the same moment as the toast, so screen readers announce nothing at all.
  • Using a fixed three-second duration, which makes any two-line message unreadable.
  • Putting the only copy of an API key, error ID or confirmation number in a toast.
  • Stealing focus when a toast appears, which throws a keyboard user out of the field they were typing in.
  • Placing toasts top-centre on mobile, where they collide with the OS notification shade.

Real-world recommendations

  • Toast frequency is the metric that matters. If a user sees more than a handful per session, the product is narrating itself instead of just working.
  • Undo-with-toast is measurably faster than confirm-then-delete for the overwhelming majority of users, and it is the pattern Gmail made standard twenty years ago.
  • Keep a notification centre for anything that would otherwise need a persistent toast. Then toasts can stay genuinely transient.
  • Log every toast with tone and title. A spike in danger toasts is usually the first signal of an incident, ahead of your error tracker.