CSS animation is the most practical tool for turning a static document into a lively, fluid experience. Without writing a single line of JavaScript, you can make buttons change color smoothly, cards glide into view, and loading indicators spin — using nothing but style rules. In this guide I'll cover the two core mechanisms separately: transition, which kicks in on state changes, and @keyframes, for timed, multi-step motion.
transition: smooth state-to-state changes
transition automatically fills in the intermediate steps whenever a property's value changes. So when a :hover, :focus, or a JavaScript-added class alters a color or size, you see a smooth blend instead of a hard jump. It has four core parts: which property, over what duration, with which easing curve, and after how much delay.
.btn {
background: #4f46e5;
transform: scale(1);
transition: background 200ms ease, transform 200ms ease;
}
.btn:hover {
background: #6366f1;
transform: scale(1.05);
}
Here the transition shorthand takes, in order, the property, the duration, and the timing function. You can list multiple properties separated by commas. Whenever possible, name the exact properties instead of using all; this keeps performance predictable and prevents properties you didn't intend from being animated.
Timing functions (easing)
Whether motion feels natural depends largely on its speed curve. In the real world objects don't start and stop abruptly; they accelerate and decelerate. CSS offers ready-made keywords and custom curves for this:
ease— the default; a balanced curve that starts and ends softly.linear— constant speed; ideal for spinning loaders.ease-out— starts fast, ends slow; feels natural for elements entering the screen.cubic-bezier(0.34, 1.56, 0.64, 1)— defines your own curve; values above 1 produce a slight "springy" overshoot.
For most UI micro-interactions, durations between 150 and 300 ms feel the most natural. Too short and the change goes unnoticed; too long and the interface feels sluggish.
@keyframes: multi-step animations
A transition only works between two states: a start and an end. For multi-stage motion — an element that grows then shrinks, or spins continuously — you use @keyframes. First you define a sequence of frames, then you apply it to an element with the animation property.
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.7; }
100% { transform: scale(1); opacity: 1; }
}
.badge {
animation: pulse 1.5s ease-in-out infinite;
}
Percentages represent points along the animation timeline; from and to are shorthands for 0% and 100%. The animation shorthand takes, in order, the name, duration, easing, iteration count (infinite for endless), and direction.
Performance: what should you animate?
A smooth animation aims for 60 frames per second. The key to hitting that is animating properties that don't force the browser to recalculate page layout. transform and opacity are handled directly on the GPU without triggering layout, which is why they should be your default choice.
- Good:
transform(translate, scale, rotate) andopacity. - Avoid:
width,height,top,left,margin— properties that trigger a reflow on every frame.
For example, to slide an element to the left, use transform: translateX() rather than left. If you want to hint to the browser that an element is about to be animated, you can add will-change: transform; — but only when you genuinely need it, since overusing it consumes memory.
Accessibility: respecting reduced-motion preferences
Some users are bothered by excessive motion or disable it at the OS level due to vestibular disorders. Honoring that preference with the prefers-reduced-motion media query is good practice:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
This rule effectively disables decorative animations while keeping the interface fully functional.
A practical example: a card that fades up into view
The example below brings a card in by moving it slightly from bottom to top and from transparent to opaque. Because it uses only transform and opacity, it stays smooth.
@keyframes fade-up {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
.card {
animation: fade-up 400ms ease-out both;
}
The both value means the animation keeps its starting state before it begins and its ending state after it finishes, so the card stays put without flickering.
Frequently Asked Questions
What's the difference between transition and animation?
transition works only between two states, usually in response to an interaction (hover, focus, class change). animation, defined with @keyframes, describes multi-step motion that can start on its own and loop.
Why do my animations stutter?
The usual culprit is animating properties like width, top, or margin that force a layout recalculation on every frame. Switching to transform and opacity resolves most stutter.
Can I trigger an animation on scroll without JavaScript?
Yes. In modern browsers, scroll-driven animations are possible with CSS animation-timeline: view();. For broader browser support, adding a class via IntersectionObserver is still the most reliable approach.
Want to give your interface a polished, fluid feel? If you need help with performant CSS animations and micro-interactions, get in touch with me.