How to Build Interruptible Toast and Notification Animations: Best Practices from emilkowalski/skills
Use CSS transitions with @starting-style, keep durations under 300ms, and animate only transform and opacity to create toast animations that can be smoothly interrupted when new notifications arrive.
Toast and notification animations must handle rapid state changes without visual jank or stacking latency. The emilkowalski/skills repository provides a comprehensive Animation Standards Reference that defines exactly how to achieve this. This guide covers the implementation patterns found in skills/review-animations/STANDARDS.md and demonstrates working code for interruptible toast systems.
Why Interruptibility Matters for Toast Animations
Transient UI elements face unique challenges that static components don't.
- Dynamic arrival patterns: Multiple toasts can trigger in seconds—form validation errors, network failures, success confirmations. A non-interruptible exit animation forces new toasts to wait, creating visible backlog.
- Compositor efficiency: CSS transitions retarget mid-animation on the GPU. Keyframe animations restart from zero, dumping work back to the main thread.
- Perceived responsiveness: Interruptible animations respond instantly to user actions, reinforcing system liveness.
The standards in emilkowalski/skills prioritize these characteristics through specific technical choices.
Core Guidelines from STANDARDS.md
The repository's animation standards establish five binding rules for toast animation interruptibility.
Prefer CSS Transitions Over Keyframes
CSS transitions can be retargeted mid-animation. When a toast's state changes, the browser smoothly interpolates from the current value to the new target. Keyframe animations (@keyframes) always restart from the initial keyframe, creating jarring visual resets when toasts are replaced.
Source: [STANDARDS.md lines 77-88](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md#L77-L88)
Leverage @starting-style for Entry States
The @starting-style at-rule defines the initial state that an element transitions from upon insertion. This eliminates JavaScript-driven entry animations, keeping the animation lightweight and fully interruptible.
@starting-style {
opacity: 0;
transform: translateY(100%);
}
Source: [STANDARDS.md lines 89-96](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md#L89-L96)
Enforce 300ms Maximum Duration
Shorter durations reduce the window where interruptions compete with in-flight animations. The standards define this as the upper bound for all transient UI animations.
Source: [STANDARDS.md lines 39-49](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md#L39-L49)
Animate Only transform and opacity
These properties are GPU-composited and bypass layout/paint cycles. Animating width, height, or other layout-triggering properties causes thrashing that destroys interruptibility under load.
Source: [STANDARDS.md lines 110-118](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md#L110-L118)
Implementation: Complete Interruptible Toast System
This section provides production-ready code implementing all standards from STANDARDS.md.
CSS Foundation with @starting-style
/* toast.css */
.toast {
/* Final visible state */
opacity: 1;
transform: translateY(0);
transition: opacity 300ms ease-out, transform 300ms ease-out;
}
/* Entry hidden state — applied automatically on insertion */
@starting-style {
.toast {
opacity: 0;
transform: translateY(100%);
}
}
/* Exit state — add this class to start dismissal */
.toast--exit {
opacity: 0;
transform: translateY(100%);
}
The @starting-style block provides the initial hidden state without JavaScript. Adding .toast--exit triggers a smooth, interruptible exit that retargets from any current position.
Source: [STANDARDS.md lines 82-88](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md#L82-L88)
React Component with Interruptible State Management
import { useState, useEffect } from "react";
import "./toast.css";
interface ToastProps {
message: string;
id: string;
onRemove: (id: string) => void;
duration?: number;
}
export const Toast = ({ message, id, onRemove, duration = 3000 }: ToastProps) => {
const [exiting, setExiting] = useState(false);
// Auto-dismiss after duration
useEffect(() => {
const timer = setTimeout(() => setExiting(true), duration);
return () => clearTimeout(timer);
}, [duration]);
// Remove from DOM only after transition completes
const handleTransitionEnd = (e: React.TransitionEvent) => {
if (e.propertyName === "opacity" && exiting) {
onRemove(id);
}
};
// setExiting(true) interrupts any in-flight animation instantly
return (
<div
className={`toast ${exiting ? "toast--exit" : ""}`}
onTransitionEnd={handleTransitionEnd}
>
{message}
</div>
);
};
Critical implementation details:
- The CSS transition ensures
setExiting(true)retargets from the current animation state onTransitionEndguarantees DOM removal only after visual completion—no abrupt pops- Calling
setExitingduring an active transition immediately redirects to exit values
Container Component with Queue Management
import { useState } from "react";
import { Toast } from "./Toast";
type ToastItem = { id: string; message: string };
export const ToastContainer = () => {
const [toasts, setToasts] = useState<ToastItem[]>([]);
const addToast = (msg: string) => {
const id = crypto.randomUUID();
setToasts((prev) => [...prev, { id, message: msg }]);
};
const removeToast = (id: string) => {
setToasts((prev) => prev.filter((t) => t.id !== id));
};
return (
<div className="toast-container">
{toasts.map((t) => (
<Toast
key={t.id}
id={t.id}
message={t.message}
onRemove={removeToast}
/>
))}
</div>
);
};
Each Toast manages its own exit transition. Adding a new toast while another animates does not block—previous transitions interrupt and new toasts slide in smoothly.
Common Interruptibility Pitfalls
Avoid these patterns that break toast animation interruptibility according to STANDARDS.md:
| Anti-pattern | Problem | Solution |
|---|---|---|
| Keyframe exit animations | Restart from frame zero on interruption | Use CSS transitions |
Animating top, left, height |
Triggers layout, drops frames | Animate transform only |
| JavaScript-driven entrance | Adds overhead, delays interruptibility | Use @starting-style |
| Durations over 500ms | Increases collision window with new toasts | Cap at 300ms |
Browser Support and Progressive Enhancement
@starting-style is supported in Chrome/Edge 117+, Safari 17.4+, and Firefox 129+. For older browsers, provide a fallback entrance animation:
/* Fallback for browsers without @starting-style support */
@supports not selector(@starting-style) {
.toast {
animation: toast-enter 300ms ease-out;
}
@keyframes toast-enter {
from {
opacity: 0;
transform: translateY(100%);
}
to {
opacity: 1;
transform: translateY(0);
}
}
}
The exit animation remains interruptible via CSS transition regardless of entrance method.
Key Standards Files in emilkowalski/skills
| File | Purpose | Location |
|---|---|---|
STANDARDS.md |
Complete animation guidelines including interruptibility, duration limits, and performance rules | [skills/review-animations/STANDARDS.md](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md) |
SKILL.md |
Applied examples of standards in concrete component implementations | [skills/review-animations/SKILL.md](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/SKILL.md) |
AUDIT.md |
Checklist for reviewing existing animations against interruptibility standards | [skills/improve-animations/AUDIT.md](https://github.com/emilkowalski/skills/blob/main/skills/improve-animations/AUDIT.md) |
Summary
- CSS transitions enable interruptible toast animations by retargeting mid-flight; keyframe animations restart and feel jarring
@starting-styleprovides declarative entry states without JavaScript overhead- 300ms maximum duration minimizes collision windows between rapid toast arrivals
transformandopacityare the only properties that guarantee GPU-composited, interruption-safe animations- State-driven exit classes allow instant redirection—critical for queue management when multiple toasts appear
Frequently Asked Questions
What makes a toast animation "interruptible"?
An interruptible animation can change direction or target values mid-flight without visual discontinuity. CSS transitions achieve this automatically: applying a new end-value interpolates from the current position. Keyframe animations cannot be interrupted—they must complete or restart from frame zero, causing jarring resets when toasts are rapidly added or removed.
Why should toast animations stay under 300ms?
Shorter durations reduce the time window where a new toast arrival conflicts with an in-progress animation. At 300ms, the probability of collision is low; at 500ms+, users frequently encounter stacked or overlapping animations that feel unresponsive. The STANDARDS.md duration table explicitly caps transient UI animations at 300ms for this reason.
Can I use Framer Motion or other libraries for interruptible toasts?
Yes, but verify they use CSS transitions under the hood. Many animation libraries default to spring physics or keyframe generation that break interruptibility guarantees. According to emilkowalski/skills standards, prefer direct CSS transitions for toast systems unless you can confirm the library retargets without restarting animations.
Does @starting-style work in all browsers?
No—@starting-style requires Chrome/Edge 117+, Safari 17.4+, or Firefox 129+. Provide @supports fallbacks using keyframe entrance animations for older browsers. The exit animation remains interruptible via CSS transition regardless, so graceful degradation is straightforward.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →