Modals ask, toasts tell. Both done right, in a handful of lines.
Modal โ use the platform
The <dialog> element handles focus trap, Esc and backdrop (full guide). The missing 10% is polish:
dialog { opacity: 0; translate: 0 12px; transition: .2s; }
dialog[open] { opacity: 1; translate: 0 0; }
@starting-style { dialog[open] { opacity: 0; translate: 0 12px; } }Toast system โ the queue pattern
const stack = document.querySelector(".toast-stack"); // fixed, bottom-right
function toast(msg, type = "info", ms = 3000) {
const el = document.createElement("div");
el.className = `toast toast-${type}`;
el.role = "status"; // screen readers announce it
el.innerHTML = `${msg} <button aria-label="Dismiss">โ</button>`;
el.querySelector("button").onclick = () => el.remove();
stack.append(el); // stacks automatically
setTimeout(() => {
el.classList.add("leaving");
el.addEventListener("transitionend", () => el.remove());
}, ms);
}
toast("Saved โ", "success");
toast("Network error โ retrying", "error", 5000);.toast-stack { position: fixed; bottom: 20px; right: 20px;
display: flex; flex-direction: column; gap: 8px; z-index: 999; }
.toast { animation: slideIn .25s ease; }
.toast.leaving { opacity: 0; translate: 20px 0; transition: .25s; }Rules: toasts never require action (that's a modal's job), errors stay longer than successes, and role="status" makes them accessible. Our playground's "Copied!" toast is this exact pattern.