9 CSS Toast Notifications — No Library, Pure CSS
A CSS toast notification is a small card that appears in a corner, holds briefly, and clears itself away so a result never interrupts what someone was doing —
Auto-plays · click a tile to jump to its section · all nine in one zip
- 01 Slide in from right
- 02 Popover API toast
- 03 3-item stack
- 04 Progress-bar auto-dismiss
- 05 Undo toast
- 06 Swipe to dismiss
- 07 Success icon
- 08 Mobile bottom toast
- 09 Notification badge
These nine follow the order a toast actually lives through rather than any popularity ranking: 01 through 03 are the noticeable ways one gets on screen, 04 through 06 are how it clears itself without a click, and 07 through 09 lean on color and shape instead of extra words to say what happened.
01Slide in from right
The card sits off-screen at translateX(140%) with opacity 0, slides in to rest, waits, then retraces the same path back out. It's the default shape for a desktop corner toast confirming a save or an upload finished.
.toast {
position: relative;
transform: translateX(140%);
opacity: 0;
}
.toast.is-demo { animation: slide-loop $dur-loop $easing infinite; }
@keyframes slide-loop {
0% { transform: translateX(140%); opacity: 0; }
18% { transform: translateX(0); opacity: 1; }
78% { transform: translateX(0); opacity: 1; }
100% { transform: translateX(140%); opacity: 0; }
}
02Popover API toast
Everything opens and closes through a popover attribute and a popovertarget button, so the browser's own top layer handles stacking plus Escape and outside-click dismissal without a line of JavaScript for either. The popover attribute reaches Chrome and Edge 114, Safari 17, and Firefox 125, though its default styling centers the element, so the corner position seen here comes from overriding that positioning by hand.
<div id="toast" class="toast" popover role="status" aria-live="polite">
<span class="toast__dot" aria-hidden="true"></span>
<p class="toast__title">Connected</p>
<button popovertarget="toast" popovertargetaction="hide" aria-label="Close notification">
<svg viewBox="0 0 24 24" width="14" height="14"><line x1="5" y1="5" x2="19" y2="19"/><line x1="19" y1="5" x2="5" y2="19"/></svg>
</button>
</div>
<script>document.getElementById('toast').showPopover();</script>
033-item stack
Three notifications rest as a barely offset deck, each card reading its own --i custom property to sit a little higher and smaller than the one below it. Hovering the group, or moving focus into it, flattens that offset back out so every card becomes individually readable.
.stack__item {
position: absolute; inset: 0;
transform: translateY(calc(var(--i) * -6px))
scale(calc(1 - var(--i) * .05));
opacity: calc(1 - var(--i) * .22);
}
.stack:hover .stack__item,
.stack:focus-within .stack__item {
transform: translateY(calc(var(--i) * -22px)) scale(1);
opacity: 1;
transition: transform $duration $easing, opacity $duration $easing;
}
04Progress-bar auto-dismiss
A bar pinned to the toast's bottom edge shrinks from scaleX(1) to scaleX(0) at a constant rate, and the toast fades out the instant that bar finishes emptying. It's built for anything that has to close on a fixed schedule, like a send or upload confirmation nobody needs to dismiss by hand.
.toast__bar {
position: absolute; left: 0; bottom: 0;
width: 100%; height: 4px;
background: $color;
transform-origin: left;
}
.toast.is-demo .toast__bar { animation: bar-shrink $dur-loop linear infinite; }
@keyframes bar-shrink {
0% { transform: scaleX(1); }
82% { transform: scaleX(0); }
100% { transform: scaleX(0); }
}
05Undo toast
Clicking Undo next to a delete message clears the pending timer and swaps the text to a cancelled state. Left alone for four seconds, the toast instead folds shut from the top edge via scaleY. It stands in for a confirm dialog on list-item deletion, where asking twice before every removal would slow the whole list down.
document.querySelectorAll('.toast').forEach(function (t) {
var btn = t.querySelector('.toast__undo'), msg = t.querySelector('.toast__text');
var timer = setTimeout(function () { t.classList.add('is-gone'); }, 4000);
btn.addEventListener('click', function () {
clearTimeout(timer);
msg.textContent = 'Cancelled';
t.classList.add('is-cancelled');
});
});
06Swipe to dismiss
Holding a pointer down and dragging left or right writes the distance into --dx, so the card tracks the finger in real time through a translateX(var(--dx)) transform. Letting go past an 80px threshold sends it flying out in that direction instead of snapping back. Touch-first chat and messenger notification trays are where the gesture reads as natural rather than gimmicky.
el.addEventListener('pointerdown', function (e) {
x0 = e.clientX; dragging = true; el.setPointerCapture(e.pointerId);
});
el.addEventListener('pointermove', function (e) {
if (!dragging) return;
dx = e.clientX - x0; el.style.setProperty('--dx', dx + 'px');
});
el.addEventListener('pointerup', function () {
dragging = false;
if (Math.abs(dx) > 80) el.classList.add(dx > 0 ? 'is-swiped-right' : 'is-swiped-left');
else el.style.setProperty('--dx', '0px');
});
07Success icon
A neutral gray ring fades out via opacity at the same moment a status-colored ring and a stroke-dashoffset checkmark fade in underneath it, so color alone carries the result rather than a second icon swap. Trading the status token for an error color turns the identical markup into a failure badge instead of a success one.
.badge__neutral { opacity: .3; }
.badge__colored { opacity: 0; }
.badge__check { stroke-dasharray: 100; stroke-dashoffset: 100; }
.toast.is-demo .badge__neutral { animation: fade-out $dur-loop $easing infinite; }
.toast.is-demo .badge__colored { animation: fade-in $dur-loop $easing infinite; }
.toast.is-demo .badge__check { animation: draw $dur-loop $easing infinite; }
08Mobile bottom toast
A pill-shaped bar pinned to the bottom edge adds env(safe-area-inset-bottom, 0px) to its own offset so it clears a phone's home indicator, then rises into place before settling back down. That function has been supported since iOS Safari 11.2, with every other browser simply reading the calc() fallback of 0px. Unlike a handled bottom sheet, it shows exactly one line and closes itself instead of waiting to be dragged away.
.toast {
position: fixed;
left: 16px; right: 16px;
bottom: calc(env(safe-area-inset-bottom, 0px) + 16px);
margin: 0 auto;
max-width: 340px;
border-radius: 999px;
transform: translateY(140%);
opacity: 0;
}
.toast.is-demo { animation: bottom-loop $dur-loop $easing infinite; }
09Notification badge
A bell icon rocks briefly around a transform-origin pinned to its own tip while the badge in the corner scales up and back on the same timing, marking the exact instant a new count arrives. It suits an unread-message badge in a header, where the motion needs to read as one event rather than a lingering state.
.bell__body {
transform-box: fill-box;
transform-origin: 50% 0%;
}
.bell.is-demo .bell__body { animation: shake $dur-loop $easing infinite; }
.bell.is-demo .badge { animation: pulse $dur-loop $easing infinite; }
@keyframes shake {
0%, 55%, 100% { transform: rotate(0deg); }
60% { transform: rotate(-14deg); }
65% { transform: rotate(12deg); }
70% { transform: rotate(-8deg); }
}
Where do these toasts actually break?
Two of the stacked-layer items break for mirror-image reasons rooted in the same habit: trusting a default instead of writing the rule explicitly. Item 07's success icon overlays a neutral ring and a colored ring in the exact same spot using grid-area: 1 / 1 on both SVGs. Drop that rule and the browser stacks them top to bottom instead of on top of each other, doubling the badge's rendered height rather than crossfading in place. Item 03's stack carries the inverse of that bug: the --i custom property has to be written inline on each card's own style attribute. A single fixed value set inside the SCSS file instead gives all three cards an identical offset, collapsing the deck into one flat card.
A couple of the other demos hid a quieter version of the same problem. Item 02's popover attribute ships with UA styling that centers itself in the viewport by default. Without an explicit position: fixed plus top, right, and margin overrides, the toast opens dead center instead of tucked into a corner. Item 06's drag needs setPointerCapture called on pointerdown, or the card's pointermove listener stops firing the moment a finger crosses the card's own edge mid-swipe, leaving the drag stuck halfway across the screen. Every one of these got caught, fixed, and re-measured before the archive shipped, and the password that opens it is vp67sjt7, typed exactly as it reads on this line with nothing added or removed.
Accessibility
All nine use the same base pairing for a screen reader:
<div class="toast" role="status" aria-live="polite">
<p>Saved</p>
</div>
That combination announces a toast without yanking focus away from whatever someone was already doing, which covers most save, upload, and connection-status messages. A login failure or a session timeout carries more urgency than that, so swapping in aria-live="assertive" (or role="alert") is the right call whenever someone genuinely has to act on what the toast says. Item 04's progress-bar auto-dismiss is the one that answers to a written accessibility rule rather than just a role attribute. WCAG 2.2.1, Timing Adjustable requires a way to extend or turn off a time limit. That's why the five-second countdown here sits next to item 05's Undo pattern instead of closing with no way to stop it. prefers-reduced-motion turns off the slide, rotation, and fan-out animations across the other eight. But item 04's bar keeps animating on purpose, since it's the only visual cue for exactly when the auto-close lands rather than decoration layered on top. MDN's prefers-reduced-motion reference documents which operating-system setting flips that media query on.
018's vibe-coding micro-interactions post stacks new toasts by pushing older ones up a slot — the opposite direction from this post's fixed three-card deck, which only fans out on hover in item 03. 003's modal animation bottom sheet is a handled panel that stays open until someone dismisses it — nothing like the one-line, self-closing pill in item 08. The rest of this site's CSS work sits on the category page, and what's actually for sale versus what's free to read is on the about page.
FAQ
Is role="status" or role="alert" correct for a toast?
role="status" paired with aria-live="polite" is right for most toasts, since it announces the message without cutting off whatever a screen reader was already reading aloud. Reach for role="alert" (or aria-live="assertive") only for messages someone truly can't miss, like a failed payment or an expired session.
How long should a toast stay up before it closes itself?
There's no single correct number, but WCAG 2.2.1 requires a way to extend, postpone, or turn off any time limit under 20 hours. Item 04 here defaults to five seconds and pairs it with item 05's Undo button so the countdown is never the only way out of a mistake.
Does the Popover API toast work in every browser?
Not yet everywhere — Chrome and Edge have supported the popover attribute since version 114, Safari since 17, and Firefox joined at 125. Feature-detecting with 'showPopover' in document.createElement('div') and falling back to a manually toggled class covers whatever's left on older browsers.