A Practical Guide to CSS Keyframes and Animation Properties
Learn how @keyframes works, what each CSS animation property does, how to build fade, slide, and bounce effects, and why animating transform and opacity keeps motion smooth.
A Practical Guide to CSS Keyframes and Animation Properties
I spent my first two years of front-end work treating CSS animation as a black box: copy a snippet, tweak numbers until it looked right, move on. Then a transform animation on a product card started janking on mid-range Android phones, and I had to actually understand what the browser was doing frame by frame. This guide is the explanation I wish I'd had then — the @keyframes syntax, every animation property, the three effects you'll write most often, and the one performance rule that matters more than the rest combined.
If you'd rather build the animation visually and copy the output, the CSS Keyframes Animation Generator does exactly that. But knowing the syntax first means you can read and debug whatever it produces.
How @keyframes actually works
A @keyframes rule is a named timeline. You declare stops as percentages from 0% to 100%, and the browser interpolates every property between them.
@keyframes fade-up {
0% {
opacity: 0;
transform: translateY(16px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
Two things trip people up. First, a property only animates if it has a defined value at both a start and an end stop — leave opacity off the 0% stop and the browser has nothing to interpolate from, so it jumps. Second, from and to are aliases for 0% and 100%; you can mix them, but I keep everything in percentages so a three-stop animation reads consistently.
The @keyframes rule does nothing on its own. It's a definition. You attach it to an element with the animation property.
The animation properties, one by one
The animation shorthand bundles up to eight longhand properties. Here are the ones you reach for daily:
animation-name— the@keyframesname to play (fade-upabove).animation-duration— how long one cycle takes, e.g.400ms. Required; without it the animation has zero duration and never visibly runs.animation-timing-function— the easing curve.ease,linear,ease-in-out,steps(n), or a customcubic-bezier(...). This is where motion gets its character.animation-delay— how long to wait before starting, e.g.200ms.animation-iteration-count— how many times to repeat. A number, orinfinitefor loops.animation-direction—normal,reverse,alternate, oralternate-reverse.alternateplays forward then backward each cycle.animation-fill-mode— what styles apply outside the run.forwardskeeps the100%values after the animation ends;backwardsapplies the0%values during the delay;bothdoes both.
Per MDN, the shorthand order is forgiving except for two rules: the first time value parsed is duration, the second is delay, and animation-name must not collide with a keyword like none. In practice the shorthand reads naturally:
.card {
animation: fade-up 400ms ease-out both;
}
The most common bug I see is a missing fill-mode. Without forwards or both, an entrance animation snaps the element back to its 0% styles the instant it finishes — the card lands, then blinks back to invisible.
A real input and its output
Say the design spec is "slide in from the left, overshoot slightly, then settle." That's three stops, not two — the overshoot needs a stop past the resting position. Here's the input I'd type and the exact CSS it produces:
Input stops: 0% → translateX(-100%), 70% → translateX(4%), 100% → translateX(0). Duration 500ms, timing-function a back-ish ease.
@keyframes slide-settle {
0% { transform: translateX(-100%); }
70% { transform: translateX(4%); }
100% { transform: translateX(0); }
}
.panel {
animation: slide-settle 500ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
The 4% overshoot at 70% is what makes it feel physical instead of robotic. That number is hard to guess from a curve alone, which is why a live preview earns its keep — you scrub to the 70% mark and confirm the overshoot reads as intentional, not as a glitch.
Three effects you'll write constantly
Fade. Animate opacity from 0 to 1. Pair it with a small translateY for a "fade-up," which feels more grounded than a flat fade. Keep duration short — 300–400ms — so it never blocks interaction.
Slide. Animate transform: translateX or translateY. Start off-screen (-100%) and end at 0. Slides are the workhorse for drawers, toasts, and panels.
Bounce. A bounce needs intermediate stops that overshoot and return. A simple attention bounce:
@keyframes bounce {
0%, 100% { transform: translateY(0); }
40% { transform: translateY(-12px); }
60% { transform: translateY(-6px); }
}
Grouping 0%, 100% on one line is a clean way to say "start and end identical." For a looping bounce, add animation-iteration-count: infinite. For a back-and-forth pulse, set direction: alternate so the timeline reverses each cycle instead of hard-cutting from 100% to 0%.
The performance rule: transform and opacity only
Here's the one thing that fixed my janky card: animate transform and opacity, almost nothing else. These two are GPU-compositable — the browser hands them to the compositor thread and skips layout and paint entirely. Animating width, height, top, or left forces a layout recalculation on every single frame, which thrashes the main thread and stutters on mobile.
So instead of animating left: 0 → 100px, animate transform: translateX(0) → translateX(100px). Instead of animating height for a reveal, animate a transform: scaleY() or clip the container. The visual result is the same; the cost is not. background-color is a reasonable exception for color transitions, but layout-triggering properties almost never belong in a 60fps animation.
If you want to fine-tune the easing curve behind any of these, the cubic-bezier editor lets you drag the control points and watch the motion change. A snappier curve on a fast slide can be the difference between "cheap" and "polished."
Wrapping up
@keyframes defines a named timeline; the animation properties decide how it plays — duration, easing, repeats, direction, and what sticks afterward. Master fill-mode so entrances don't blink back, reach for direction: alternate on loops, and keep your animated properties to transform and opacity so motion stays buttery on real devices. Build a few by hand to internalize the syntax, then lean on a visual generator to move fast.
Made by Toolora · Updated 2026-06-13