CSS Animations: A Practical Guide to Keyframes and Transitions (2026)
Vikas Sharma
Senior Frontend Developer

Motion is what makes an interface feel alive: a button that responds, a menu that slides, a loader that reassures. CSS handles most of it without a single line of JavaScript, through two tools that get confused constantly, transitions and animations. This guide untangles them, shows you which properties to animate for buttery-smooth results, and covers the easing and accessibility details that turn motion from gimmicky into polished.
Transition vs animation: which do you need?
A transition animates a property from its current value to a new one in response to a change, usually a hover, focus, or class toggle. It needs a trigger and it runs once, A to B. An animation, defined with @keyframes, runs on its own, can loop forever, and can pass through many intermediate states. Reach for a transition for state changes; reach for an animation for anything ongoing or multi-step.
| Transition | Animation | |
|---|---|---|
| Needs a trigger | Yes (hover, class change) | No, can autoplay |
| Can loop | No | Yes (infinite) |
| Intermediate steps | No, just start and end | Yes, any number of keyframes |
| Best for | Hover, focus, toggles | Loaders, attention, entrances |
Transitions in practice
A transition needs three things: a property to watch, a duration, and ideally an easing curve. Declare it on the resting state, not the hover state, so it animates in both directions.
.button {
background: #1B4DFF;
transform: translateY(0);
transition: transform 0.2s ease, background 0.2s ease;
}
.button:hover {
transform: translateY(-2px);
background: #163fd6;
}Avoid transition: all. It looks convenient but forces the browser to watch every animatable property, which can trigger surprise animations and hurt performance. List the properties you actually change.
Writing @keyframes animations
An animation has two parts: a @keyframes block defining the stages, and an animation property applying it to an element.
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: fade-in-up 0.5s ease-out both;
}from and to are shorthand for 0% and 100%. For multi-step motion, use percentages. The both keyword (a fill-mode) keeps the element at its first frame before the animation starts and its last frame after it ends, preventing the flash you get otherwise.
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.08); opacity: 0.7; }
100% { transform: scale(1); opacity: 1; }
}
.badge { animation: pulse 1.6s ease-in-out infinite; }The only properties you should animate
This is the most important performance rule in CSS animation. The browser renders in stages: layout, paint, composite. Animating a property that triggers layout (like width, height, top, margin) forces the browser to recalculate the position of everything on every frame, which drops you below 60fps on anything but a trivial page.
- transform (translate, scale, rotate) - composited, cheap, animate freely
- opacity - composited, cheap, animate freely
- Avoid: width, height, top, left, margin, padding - these trigger layout
- Avoid animating box-shadow and background directly - they trigger paint every frame; animate a layered pseudo-element opacity instead
Want to move something? Use transform: translateX() instead of left. Want it bigger? transform: scale() instead of width. Ninety percent of smooth web animation is just transform and opacity.
Easing: the difference between cheap and premium
Linear motion (a constant speed) looks robotic because nothing in the real world moves that way. Easing curves add acceleration and deceleration. The built-in keywords cover most needs:
- ease-out - fast start, slow finish; the best default for things entering or responding to a user
- ease-in - slow start, fast finish; good for things leaving the screen
- ease-in-out - slow at both ends; natural for looping motion
- cubic-bezier(...) - custom curves when you want a specific feel, including subtle overshoot for a bouncy snap
For UI that responds to a user, ease-out almost always feels right: the element reacts instantly then settles. A custom cubic-bezier(0.34, 1.56, 0.64, 1) adds a touch of overshoot that reads as playful and modern.
Respect reduced motion
Some users get motion sickness or distraction from animation and set a system preference for reduced motion. Honoring it is an accessibility requirement, not a nice-to-have. Wrap non-essential motion in a media query:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}This near-instant override keeps your interface functional while removing the motion for users who asked not to see it.
Hand-writing @keyframes and dialing in easing curves is slow. The CSS Animation Generator lets you build keyframe animations visually, preview them live, and tweak timing and easing, then copies ready-to-use code into your stylesheet.
Conclusion
Use transitions for state changes and @keyframes for ongoing or multi-step motion. Animate transform and opacity and almost nothing else. Pick ease-out for responsive UI, list your transition properties explicitly, and always honor prefers-reduced-motion. Follow those rules and your motion will feel fast, intentional, and accessible instead of janky and decorative.
Table of Contents
Share Article
Vikas Sharma
Senior Frontend Developer
Senior Frontend Developer with 8+ years of experience building production CSS layouts.