Toast
A transient notification that slides in from the bottom. Toasts confirm an action happened, surface a soft error, or nudge without interrupting. They auto-dismiss after 4s — never modal, never blocking. Max 2 lines, optional action.
Variants
Info (ink background) is the default — used for neutral confirmations like "expense added" or "group created." It doesn't require any action.
Success (teal background) is reserved for moments of resolution: a balance cleared, a group fully settled. Appears with confetti or celebratory language. Has an optional share action.
Error (coral background) surfaces a failure that the user should know about and may need to act on. Always includes a retry or dismiss action. Never use it for warnings — only actual failures.
Sizes
| Property | Value | Notes |
|---|---|---|
| Max width | 360px | Centers on wider viewports |
| Border radius | 14px (--r-md) | Softer than a card, sharper than a pill |
| Padding | 13px 16px | Comfortable without feeling bloated |
| Max lines | 2 | Title + 1 line of message. Clamp at 2 with ellipsis. |
| Z-index | --z-toast (200) | Above modals at 100, below tooltip at 300 |
States
| State | Animation | Duration |
|---|---|---|
| Enter | translateY(100%) → translateY(0) + opacity 0 → 1 | 240ms --ease-out |
| Resting | Static | 4000ms (auto-dismiss timer) |
| Exit (auto) | translateY(0) → translateY(100%) + opacity 1 → 0 | 240ms --ease-out |
| Exit (action) | Same as auto-exit — triggered immediately on action tap | 240ms --ease-out |
On dark surfaces
Toast colors are self-contained — ink, teal, and coral all render correctly on dark backgrounds. No dark-mode variants needed. The toasts are opaque; the background behind them doesn't affect the text contrast.
Anatomy
| Part | Element | Notes |
|---|---|---|
| Container | .toast | 14px radius, shadow-float. Rendered in a portal at the bottom of the viewport. |
| Icon | .toast-icon | Optional. 18px SVG, 85% opacity for visual softness. Stroked, not filled. |
| Body | .toast-body | Flex column. Title (600 weight) + message (75% opacity, 2-line clamp). |
| Action | .toast-action | Optional. Right-aligned. Semi-transparent background on the colored surface. Use for undo, retry, view — not dismiss. |
Usage
Accessibility
role="status" and aria-live="polite" for informational toasts. Error toasts use role="alert" and aria-live="assertive" — they interrupt the screen reader immediately.
prefers-reduced-motion: reduce, skip the slide animation. The toast appears and disappears instantly — no movement. The 4s auto-dismiss timer is unaffected.
Code
HTML structure
<!-- Portal container — one per page, lives at body root --> <div class="toast-portal" role="status" aria-live="polite" aria-atomic="true"></div> <!-- Info toast (inject into portal) --> <div class="toast toast-info"> <div class="toast-icon"><!-- SVG --></div> <div class="toast-body"> <span class="toast-title">Expense added</span> <span class="toast-message">₹840 Swiggy split with Priya</span> </div> </div> <!-- Error toast with retry --> <div class="toast toast-error" role="alert"> <div class="toast-body"> <span class="toast-title">Payment failed</span> <span class="toast-message">UPI timed out. Try again.</span> </div> <button class="toast-action">Retry</button> </div>
JavaScript — show + auto-dismiss
function showToast({ variant = 'info', title, message, action, onAction, duration = 4000 }) { const portal = document.querySelector('.toast-portal'); const toast = document.createElement('div'); toast.className = `toast toast-${variant}`; toast.innerHTML = ` <div class="toast-body"> <span class="toast-title">${title}</span> ${message ? `<span class="toast-message">${message}</span>` : ''} </div> ${action ? `<button class="toast-action">${action}</button>` : ''} `; if (onAction) { toast.querySelector('.toast-action')?.addEventListener('click', () => { onAction(); dismiss(); }); } portal.appendChild(toast); function dismiss() { toast.classList.add('toast-out'); toast.addEventListener('animationend', () => toast.remove(), { once: true }); } const timer = setTimeout(dismiss, duration); toast.addEventListener('mouseenter', () => clearTimeout(timer)); toast.addEventListener('mouseleave', () => setTimeout(dismiss, duration)); toast.addEventListener('focusin', () => clearTimeout(timer)); toast.addEventListener('focusout', () => setTimeout(dismiss, duration)); }
Design tokens used
| Token | Value | Role |
|---|---|---|
--ink | #171513 | Info variant background |
--teal | #0E7C66 | Success variant background |
--coral | #E76F51 | Error variant background |
--s-paper | #FBF8F0 | Text on info + success toasts |
--shadow-float | 0 30px 80px -30px rgba(23,21,19,.28) | Toast elevation |
--r-md | 14px | Toast border radius |
--d-med | 240ms | Enter/exit animation duration |
--ease-out | cubic-bezier(0.23,1,0.32,1) | Slide easing |
--z-toast | 200 | Stack order |