CSS Animations and Keyframes Explained
How CSS keyframe animations actually work: timing functions, fill modes, composition, and the patterns that keep them smooth and predictable.
What you'll learn
- ✓How @keyframes and animation properties connect
- ✓What fill-mode and direction do
- ✓Which properties animate cheaply on the GPU
- ✓How to chain and stagger animations
- ✓How to avoid jank and reflows
Prerequisites
- •Comfortable with HTML and CSS
CSS animations are deceptively simple at first glance and surprisingly deep in practice. You write @keyframes, attach an animation shorthand, and the browser does the rest. But the difference between an animation that feels delightful and one that feels broken often comes down to which properties you animate and how you compose multiple animations together.
What animations are and why they matter
A CSS animation interpolates property values over time according to a set of keyframes. Unlike transitions, which only run in response to a state change, animations run on their own schedule. You can loop them, chain them, pause them, and orchestrate them entirely from the stylesheet.
Animations matter because motion communicates. A subtle slide tells the user where something came from. A pulse draws the eye to a call to action. Done well, motion makes interfaces feel alive. Done poorly, it makes them feel slow and broken.
Mental model
Think of an animation as a function from time to property values. @keyframes defines the function. The animation shorthand attaches the function to an element and specifies how long to run, when to start, how to ease, and what to do when it ends.
@keyframes slide-in {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
animation: slide-in 0.3s ease-out 0.1s forwards;
| | | | |
name dur easing delay fill-mode
Fill-mode is the most misunderstood part. Without fill-mode: forwards, the element snaps back to its pre-animation state after finishing. With forwards, it keeps the final keyframe values. With backwards, it adopts the first keyframe values during the delay.
Hands-on example
Let us build a small toast notification that slides up, holds, and slides down. We will use two animations chained together.
.toast {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
padding: 0.75rem 1rem;
background: #111;
color: white;
border-radius: 0.5rem;
opacity: 0;
animation: toast-in 0.25s ease-out 0s forwards,
toast-out 0.25s ease-in 2.5s forwards;
}
@keyframes toast-in {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes toast-out {
to { opacity: 0; transform: translateY(20px); }
}
Two animations run on the same element with different delays. The first one rises and fades in over 0.25 seconds. The second one waits 2.5 seconds, then fades out. Both use forwards so the toast actually stays in its final state. Without forwards on the out animation, the toast would suddenly reappear at full opacity when the animation ends.
time 0s 0.25s 2.5s 2.75s
| | | |
in [ ===>===> ]
hold [================ ]
out [ ===>===> ]
Notice we animated transform and opacity, not top or height. That is intentional. The browser compositor can move and fade an element on the GPU without re-laying out the page. Animating top, width, or margin forces a layout pass on every frame, and on a busy page that means dropped frames.
Common pitfalls
The first is animating expensive properties. width, height, top, left, margin, and padding all trigger layout. On a simple page that might be fine, but on a complex one it stutters. transform and opacity are the safe defaults.
The second is forgetting fill-mode. The element finishes the animation, then snaps back. Developers add a setTimeout to set a class and re-implement what fill-mode: forwards already does for free.
The third is competing transforms. If you animate transform: translateY in one keyframe and also have transform: scale on hover, the two fight each other. CSS does not compose them automatically. You have to combine them in the same property: transform: translateY(0) scale(1.05).
A fourth is ignoring prefers-reduced-motion. Some users actively disable motion due to vestibular issues. Wrap non-essential animations in a media query that respects their preference.
@media (prefers-reduced-motion: reduce) {
.toast { animation: none; opacity: 1; }
}
Best practices
Keep durations small. UI motion in the 150 to 300 millisecond range feels responsive. Longer animations should be reserved for meaningful transitions where the duration carries information.
Use easing curves that match the motion. Ease-out for entrances (fast then slow, like something settling into place). Ease-in for exits (slow then fast, like something leaving). Cubic-bezier for custom personality.
Compose with delays instead of nesting. If you want a list of items to stagger in, give each item the same animation with a different delay. CSS variables make this easy.
.item { animation: slide-in 0.3s ease-out var(--d) forwards; }
.item:nth-child(1) { --d: 0ms; }
.item:nth-child(2) { --d: 60ms; }
.item:nth-child(3) { --d: 120ms; }
Profile in dev tools. The Performance panel shows you which frames dropped and why. If you see purple bars (layout) during your animation, you are animating the wrong property.
Wrap-up
CSS animations are a small surface area with a lot of leverage. The key ideas are: keyframes define a function over time, the animation shorthand attaches it, fill-mode preserves end state, and only transform and opacity animate cheaply. Stay inside those rails and your motion will be smooth on a laptop, smooth on a phone, and respectful of users who prefer no motion at all. That combination is what separates polished interfaces from twitchy ones.
Related articles
- HTML & CSS CSS Transitions and Keyframe Animations
A practical guide to CSS transitions and @keyframes animations — properties, timing functions, transform-based performance, the animation shorthand, and prefers-reduced-motion.
- HTML & CSS CSS Cascade Layers and Specificity: A Calmer Mental Model
Stop fighting !important. Learn how the CSS cascade, specificity, and the new @layer rule combine to give you predictable, maintainable styles.
- HTML & CSS CSS Clamp and Fluid Typography
Use the CSS clamp function to build fluid typography and spacing that scales smoothly between breakpoints without media queries or jarring jumps.
- HTML & CSS CSS Container Queries Explained with Real Examples
Container queries let components style themselves based on their parent's size, not the viewport. Learn the syntax, the units, and when to use them.