Alerts and toasts
Four inline alert types plus a stacking toast system with correct live regions.
<div class="alerts">
<div class="alert info">
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 8h.01"/></svg>
<div><strong>Scheduled maintenance</strong>
<p>The API will be read-only on Sunday between 02:00 and 04:00 UTC.</p></div>
</div>
<div class="alert ok" role="status">
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M8.5 12.5l2.5 2.5 4.5-5"/></svg>
<div><strong>Project deployed</strong>
<p>Live at <a href="#">acme.example.com</a> — took 38 seconds.</p></div>
</div>
<div class="alert warn">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4l9 16H3z"/><path d="M12 10v4M12 17h.01"/></svg>
<div><strong>Your card expires next month</strong>
<p><a href="#">Update your payment details</a> to avoid an interruption.</p></div>
<button class="close" type="button" aria-label="Dismiss this notice">×</button>
</div>
<div class="alert bad" role="alert">
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M15 9l-6 6M9 9l6 6"/></svg>
<div><strong>Build failed</strong>
<p>Module not found: <code>./utils/format</code> at line 12 of <code>app.js</code>.</p></div>
</div>
</div>
<div class="try">
<button class="btn" type="button" data-toast="ok">Show a success toast</button>
<button class="btn ghost" type="button" data-toast="bad">Show an error toast</button>
</div>
<!-- toasts land here; the region exists from page load so it is watched -->
<div class="toasts" id="toasts" role="region" aria-label="Notifications"></div>* { box-sizing: border-box; margin: 0; }
body { font: 16px/1.6 system-ui, -apple-system, sans-serif; padding: 22px; background: #f6f8fb; }
.alerts { max-width: 620px; margin: 0 auto; display: grid; gap: 12px; }
/* ============ inline alert ============ */
.alert {
--tint: #eef1f6;
--line: #dbe2ec;
--ink: #5b6b83;
display: flex;
gap: 12px;
padding: 14px 16px;
border: 1px solid var(--line);
border-left: 3px solid var(--ink);
border-radius: 10px;
background: var(--tint);
}
.alert.info { --tint: #eef0fe; --line: #dbe0fd; --ink: #2a3abf; }
.alert.ok { --tint: #eaf7f1; --line: #cdeade; --ink: #1a7a50; }
.alert.warn { --tint: #fdf6e7; --line: #f5e4bd; --ink: #9a6414; }
.alert.bad { --tint: #fdeeee; --line: #f6d5d5; --ink: #b03535; }
.alert svg {
width: 19px; height: 19px;
flex-shrink: 0;
margin-top: 2px;
fill: none;
stroke: var(--ink);
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
}
.alert strong { display: block; font-size: .93rem; color: #16202e; }
.alert p { font-size: .88rem; color: var(--ink); margin-top: 2px; }
.alert a { color: var(--ink); font-weight: 500; }
.alert code { background: rgba(255,255,255,.7); padding: 1px 5px; border-radius: 4px; font-size: .88em; }
.close {
margin-left: auto;
align-self: flex-start;
width: 26px; height: 26px;
display: grid; place-items: center;
font: inherit; font-size: 1.1rem; line-height: 1;
border: 0; border-radius: 6px;
background: none;
color: var(--ink);
opacity: .6;
cursor: pointer;
}
.close:hover { opacity: 1; background: rgba(255,255,255,.6); }
.close:focus-visible { outline: 2px solid currentColor; outline-offset: 1px; }
/* ============ toasts ============ */
.try { max-width: 620px; margin: 22px auto 0; display: flex; gap: 10px; flex-wrap: wrap; }
.btn {
font: inherit; font-size: .92rem; font-weight: 500;
padding: 10px 20px; min-height: 40px;
border: 1px solid #3b4fe4; border-radius: 9px;
background: #3b4fe4; color: #fff; cursor: pointer;
}
.btn:hover { background: #2a3abf; border-color: #2a3abf; }
.btn.ghost { background: #fff; color: #16202e; border-color: #d9e0ea; }
.btn.ghost:hover { background: #f2f5f9; }
.toasts {
position: fixed;
right: 16px;
bottom: 16px;
z-index: 100;
display: grid;
gap: 10px;
justify-items: end;
pointer-events: none; /* the stack must not block the page */
}
.toast {
pointer-events: auto; /* the toasts themselves are clickable */
display: flex;
align-items: center;
gap: 10px;
max-width: 340px;
padding: 12px 14px;
border-radius: 10px;
background: #16202e;
color: #fff;
font-size: .89rem;
box-shadow: 0 4px 8px rgba(22,32,46,.1), 0 18px 40px -22px rgba(22,32,46,.7);
animation: rise .22s ease-out;
}
.toast.bad { background: #b03535; }
.toast .x { margin-left: auto; border: 0; background: none; color: inherit; opacity: .6;
font-size: 1.05rem; line-height: 1; cursor: pointer; padding: 2px 4px; }
.toast .x:hover { opacity: 1; }
@keyframes rise { from { opacity: 0; transform: translateY(10px); } }
@media (prefers-reduced-motion: reduce) {
.toast { animation: none; }
}
@media (max-width: 480px) {
.toasts { left: 16px; justify-items: stretch; }
.toast { max-width: none; }
}// dismissable inline alerts
document.querySelectorAll(".alert .close").forEach((b) => {
b.addEventListener("click", () => b.closest(".alert").remove());
});
const stack = document.getElementById("toasts");
function toast(message, kind = "ok", ms = 4000) {
const el = document.createElement("div");
el.className = "toast " + kind;
// errors interrupt; confirmations wait for a pause
el.setAttribute("role", kind === "bad" ? "alert" : "status");
const text = document.createElement("span");
text.textContent = message;
const close = document.createElement("button");
close.className = "x";
close.type = "button";
close.setAttribute("aria-label", "Dismiss");
close.textContent = "\u00d7";
close.addEventListener("click", () => el.remove());
el.append(text, close);
stack.append(el);
// errors stay until dismissed; anything else clears itself
if (kind !== "bad") setTimeout(() => el.remove(), ms);
}
document.querySelectorAll("[data-toast]").forEach((b) => {
b.addEventListener("click", () => {
b.dataset.toast === "bad"
? toast("Could not save — the server did not respond.", "bad")
: toast("Saved. Your changes are live.", "ok");
});
});How it works
role="status" versus role="alert". This is the whole accessibility story for notifications and it gets skipped constantly. status is polite: the screen reader finishes its current sentence and then announces. alert interrupts immediately. Use alert only for errors and things that need action now; use status for confirmations. An alert on every "Saved" message makes a screen reader unusable.
The toast container exists from page load. A live region has to be in the DOM before content is inserted into it — if you create the container and its message in the same tick, many screen readers announce nothing at all. That is the most common toast bug there is, and it is invisible unless you test with a screen reader.
Errors do not auto-dismiss. A four-second timeout on a message someone needs to act on is hostile: people reading slowly, using magnification, or simply looking elsewhere will miss it. Confirmations expire; errors wait.
pointer-events: none on the stack, auto on the toasts. Without this, the invisible container sits over the bottom-right corner of your page and eats clicks on anything underneath.
Custom properties drive the four variants. Three variables per variant — tint, border, ink — and the icon picks up the ink colour through stroke: var(--ink).
Accessibility notes
Icons are aria-hidden because each alert has a heading that says the same thing in words. A red triangle alone means nothing to a screen reader.
The dismiss buttons have descriptive labels rather than reading out "times" or "x".
Colour is never the only signal: each variant has an icon, a bold heading and a coloured left border, so the four types are distinguishable in greyscale.
Test toasts with a screen reader before shipping them. Announcements that work in one browser and silently fail in another are the norm in this component, and the timing of when the region enters the DOM is usually the cause.
Making it yours
Toasts belong bottom-right on desktop and full-width at the bottom on mobile, which is what the 480-pixel rule does. Top-centre is also defensible; top-right collides with browser download prompts and extension popups.
Cap the stack at three or four. Beyond that, older messages should be replaced rather than queued — a wall of toasts is noise.
For an inline alert that must not be missed, put it above the content it concerns and move focus to it. A dismissable banner at the top of the page is for information; an alert next to a failing field is for action.
Related templates
Button set
Primary, secondary, ghost and danger, in three sizes, with every state covered.
HTMLCSSTailwindLoaders and skeletons
Six loading indicators in pure CSS, plus the skeleton pattern that usually beats all of them.
HTMLCSSModal dialog
A modal built on the native dialog element — focus trap and backdrop included, for free.
HTMLCSSJavaScriptAccordion and FAQ
Built on details and summary — open, close and keyboard support with no JavaScript.
HTMLCSSCheck your version
Once you have edited this, the HTML validator will catch any tag you left unclosed, and the formatter will tidy the indentation. Both run in your browser.