AniUI Academy

Build a toast notification system

mediumState & Data+200 XP

A button triggers a toast notification. Triggering it again while toasts are already showing stacks another one alongside them, rather than replacing it.

Each toast disappears on its own after 2 seconds, or immediately if its own close button is clicked.

Examples

// Click "Show toast" three times in quick succession:
//   [ toast 1 (×) ]
//   [ toast 2 (×) ]
//   [ toast 3 (×) ]
// 2 seconds later, with no interaction: all three disappear.
// Click × on toast 2 immediately → only toast 2 goes; 1 and 3 still auto-dismiss.

Each toast tracks its own 2s timer — closing one doesn't reset the others.

function showToast() {
  const id = nextId++;
  setToasts((current) => [...current, { id, text: "Saved!" }]);
  setTimeout(() => {
    setToasts((current) => current.filter((t) => t.id !== id));
  }, 2000);
}

function closeToast(id) {
  setToasts((current) => current.filter((t) => t.id !== id));
}

Filtering by id (not index) is what lets the auto-dismiss and the manual close coexist safely.

What it has to do

  • A button `data-testid="show-toast"` adds one toast each time it is clicked.
  • Each toast renders as `data-testid="toast"` inside a `data-testid="toast-container"`.
  • Multiple toasts triggered in quick succession all stack simultaneously — the count of visible toasts matches the number of clicks.
  • Each toast has its own `data-testid="toast-close"` button that removes only that toast.
  • A toast that is not manually closed removes itself automatically after 2000ms.

Notes

  • Keep a stable id per toast — closing by array index breaks when a mid-stack toast auto-dismisses first and shifts the indices.
  • If you store the timer id on the toast you can `clearTimeout(toast.timer)` in `closeToast()` to drop the dead callback — not required by the test, but avoids the setState-after-unmount warning in strict mode.
  • Position the container with `position: fixed` top/right (or bottom/center) so toasts float over whatever page content is beneath. Accessibility: wrap it in `role="status" aria-live="polite"` so screen readers announce new toasts.
  • A production toast system usually caps the stack at 3-5 and queues the rest — a lesson learned the first time an API error loop produces 400 toasts in 10 seconds.
Asked atStripeShopifyAtlassian

Stuck? The react course covers the ideas this problem is built on.

import { useState } from "react";

let nextId = 1;

export default function App() {
  const [toasts, setToasts] = useState([]);

  function showToast() {
    const id = nextId++;
    setToasts((current) => [...current, { id, message: "Action completed" }]);
    // TODO: this toast should remove itself automatically after 2000ms.
  }

  function closeToast(id) {
    // TODO: remove just this toast from state.
  }

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      <button data-testid="show-toast" onClick={showToast}>
        Show toast
      </button>

      <div
        data-testid="toast-container"
        style={{ marginTop: 16, display: "flex", flexDirection: "column", gap: 8 }}
      >
        {toasts.map((toast) => (
          <div
            key={toast.id}
            data-testid="toast"
            style={{ padding: 12, background: "#333", color: "white", borderRadius: 6 }}
          >
            {toast.message}
            <button
              data-testid="toast-close"
              onClick={() => closeToast(toast.id)}
              style={{ marginLeft: 12 }}
            >
              &times;
            </button>
          </div>
        ))}
      </div>
    </div>
  );
}

5 tests · +200 XP

Discussion

Sign in to post
Sign in to join the conversation.Sign in

No comments yet. Be the first to say something.