Motion is a design material. Most developers treat it as a finishing touch — something you add after the layout is done. But the best interfaces treat motion the way they treat type: as a primary element with its own grammar, hierarchy, and intention.
This is about learning that grammar.
Easing is everything
The single biggest difference between animation that feels cheap and animation that feels crafted is the easing curve. Duration matters. But easing matters more.
The browser gives you five keywords: linear, ease, ease-in, ease-out, ease-in-out. These are fine as defaults and terrible as a final answer.
cubic-bezier() gives you full control:
.element {
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
That specific curve — overshoot past 1.0 on the y-axis — produces a spring-like bounce at the end. The element arrives at its destination and slightly overshoots before settling. It feels physical.
Understanding the four values:
cubic-bezier(x1, y1, x2, y2)
x1, y1— the first control point (affects the start of the curve)x2, y2— the second control point (affects the end of the curve)xvalues must stay between 0 and 1 (time)yvalues can go outside 0–1 (producing overshoot)
A few curves worth knowing:
/* Snappy entrance — fast start, gentle landing */
cubic-bezier(0.22, 1, 0.36, 1)
/* Spring — overshoots and settles */
cubic-bezier(0.34, 1.56, 0.64, 1)
/* Anticipation — slight pullback before moving */
cubic-bezier(0.36, 0, 0.66, -0.56)
/* Smooth deceleration — for elements entering the screen */
cubic-bezier(0, 0, 0.2, 1)
The anticipation curve goes negative on y — the element moves backward slightly before accelerating forward. Used on a button press, it feels like something winding up.
Duration and the perception of weight
Duration communicates weight. Fast animations feel light. Slow animations feel heavy or important.
A general framework:
- 50–100ms — micro-interactions: hover states, focus rings, checkbox ticks
- 150–300ms — UI transitions: dropdowns, tooltips, tab changes
- 300–500ms — layout changes: modals, drawers, page sections
- 500ms+ — cinematic: hero entrances, loading sequences, brand moments
Breaking these rules is valid — but do it intentionally. A button hover that takes 400ms doesn’t feel crafted, it feels broken.
Staggered animations — choreography
Stagger is where individual animations become choreography. Instead of all elements moving at once, they move in sequence — each delayed slightly from the previous.
.card:nth-child(1) { animation-delay: 0ms; }
.card:nth-child(2) { animation-delay: 60ms; }
.card:nth-child(3) { animation-delay: 120ms; }
.card:nth-child(4) { animation-delay: 180ms; }
But hardcoded delays are brittle. CSS custom properties make stagger scalable:
.card {
animation: fade-up 0.5s cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: calc(var(--i) * 60ms);
}
Set --i on each element:
<div class="card" style="--i: 0">...</div>
<div class="card" style="--i: 1">...</div>
<div class="card" style="--i: 2">...</div>
The stagger is now a single number you can tune globally. Change 60ms to 40ms and every card responds.
Direction and hierarchy in stagger
Not all staggers are linear. The direction of the stagger communicates hierarchy:
Top to bottom — reading order, content flowing in naturally Bottom to top — rare, creates urgency, feels like things are rising Center outward — emphasizes the middle element as most important Random — organic, chaotic, used for particle-like effects
/* Stagger from center outward */
.card {
--distance-from-center: abs(calc(var(--i) - var(--center)));
animation-delay: calc(var(--distance-from-center) * 50ms);
}
CSS abs() is available in modern browsers. Combined with a --center variable set to half the total count, elements near the center animate first and edges follow.
animation-fill-mode: both
One of the most common animation bugs: elements flash into their pre-animation state before the animation starts (when there’s a delay), then snap to their final state after it ends.
animation-fill-mode fixes this:
forwards— holds the final keyframe state after animation endsbackwards— applies the first keyframe during the delay periodboth— does both
.card {
animation: fade-up 0.5s ease both;
animation-delay: calc(var(--i) * 60ms);
}
@keyframes fade-up {
from {
opacity: 0;
transform: translateY(16px);
}
}
With both, the card is invisible during its delay (applying the from keyframe), then animates in, then holds at opacity: 1, translateY(0).
Scroll-driven choreography
CSS scroll-driven animations change what choreography means. Instead of time-based stagger, you can stagger based on position in the viewport:
@keyframes reveal {
from {
opacity: 0;
transform: translateY(20px);
}
}
.card {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% entry 30%;
}
Every card reveals itself as it enters the viewport — no JavaScript, no IntersectionObserver, no class toggling. The stagger happens naturally because cards enter the viewport at different times as the user scrolls.
The prefers-reduced-motion rule
Motion can cause problems for people with vestibular disorders. prefers-reduced-motion is not optional:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
This is a blanket override — all animations collapse to near-instant. Some designers prefer a more nuanced approach: keeping functional transitions (like a focus ring appearing) but removing decorative ones (like a page entrance animation).
@media (prefers-reduced-motion: reduce) {
.decorative-animation {
animation: none;
}
}
Either way, respect the preference. Motion that can’t be turned off is an accessibility failure.
Motion as design language
The best motion design isn’t noticed. It’s felt.
When a modal opens with a slight upward drift and a soft spring at the end, users don’t think “nice easing.” They think the interface feels responsive and alive. When a list staggers in from top to bottom, users don’t parse the delay values — they just feel like the content arrived in a natural, readable order.
Motion communicates:
- Speed — how fast something responds tells you how much it cares about your action
- Weight — heavy things move slowly, light things move quickly
- Relationship — elements that move together belong together
- Hierarchy — what moves first is most important
These are design decisions, not developer decisions. But they’re implemented in code. That’s the design engineer’s territory: understanding motion as a design language and having the technical fluency to execute it precisely.
The easing curve is a sentence. Learn to write it well.