Two tools: transitions & keyframes
CSS gives you two ways to move things, and picking the right one is half the battle:
| Tool | Animates | Best for |
|---|---|---|
| transition | From state A → B when something changes (hover, a class toggle) | Hovers, buttons, opening panels, micro-interactions. |
| @keyframes + animation | A multi-step sequence that can loop on its own | Spinners, looping effects, attention-grabbers, entrances. |
Transitions: smooth A → B
A transition tells the browser to animate a change instead of snapping. You set it once; it fires whenever the value changes. Four parts: property, duration, timing function, optional delay.
.box { transition: transform .35s ease; property duration easing } .box:hover { transform: scale(1.15); } the change that gets animated
transition: transform .35s ease, background .35s ease; or animate everything with transition: all .3s ease (handy, but a touch less efficient than naming properties).What to animate (and what to avoid)
Not all properties are equal. Some are cheap for the browser to animate at 60fps; others force expensive recalculation of the whole layout and cause jank.
| Animate these (fast) | Avoid animating (slow) |
|---|---|
transform (move, scale, rotate) | width, height, top, left, margin |
opacity (fades) | box-shadow on big areas, filter in loops |
transform: translate() instead of left/top, and fade with opacity. These two are GPU-accelerated and don't trigger layout, so they stay buttery even on phones.transform: move, scale, rotate, skew
transform is the workhorse. It repositions an element visually without disturbing the layout around it. Click each to see it (with a springy easing):
transform: translateY(-30px) rotate(20deg) scale(1.2) applies in order. Transforms also take a transform-origin (default the center) if you want to rotate/scale from a corner.Easing: the personality of motion
The timing function controls how speed changes over the duration. linear is robotic; real motion accelerates and decelerates. Hit Run — all four balls travel the same distance in the same time, but feel completely different:
transition-timing-function: ease-in-out; transition-timing-function: cubic-bezier(.34, 1.56, .64, 1); overshoots = bouncy
ease (the default) suits most UI. ease-out feels responsive for things entering. A cubic-bezier that goes above 1 overshoots and springs back — that's the playful bounce.@keyframes: multi-step sequences
When you need more than a single A → B, define the steps with @keyframes — using from/to or percentages — then attach it with the animation property. Hit replay:
@keyframes dance { 0% { transform: translateY(0) rotate(0); } 30% { transform: translateY(-40px); } 60% { transform: rotate(180deg); border-radius: 50%; } 100% { transform: rotate(360deg); } } .box { animation: dance 1.4s ease-in-out; }
0% is the same as from, 100% the same as to.The animation properties
The animation shorthand bundles several controls. Know them individually — especially iteration, direction, and fill-mode:
| Property | Controls |
|---|---|
| animation-name | Which @keyframes to run. |
| animation-duration | How long one cycle takes (1.4s). |
| animation-timing-function | The easing (same options as transitions). |
| animation-delay | Wait before starting. |
| animation-iteration-count | How many times — a number or infinite. |
| animation-direction | normal, reverse, or alternate (ping-pong). |
| animation-fill-mode | forwards keeps the final frame after it ends. |
animation: pulse 1.5s ease-in-out 0s infinite alternate; /* name dur easing delay count direction */
infinite + alternate is the recipe for a smooth, endlessly breathing effect (like the pulse in the gallery below). Use forwards when an entrance animation should stay at its end state instead of snapping back.The effects you'll reach for
Most real-world animation is a handful of recognizable patterns. Here they are, all looping live:
rotate(360deg) loop) is your loading spinner. fade + slide together make the classic “fade-in-up” entrance. shake is great for signalling an invalid form field.Keep it fast & accessible
- Animate only
transformandopacityfor anything that moves often or loops (Step 3). - Keep UI animations short — 150–400ms. Long animations feel sluggish.
- Animate with purpose: feedback, guiding the eye, showing relationships. Motion for its own sake distracts.
- Always honor
prefers-reduced-motion— some users feel ill from motion. This very page disables its animations when that's set.
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
prefers-reduced-motion is a one-block media query that prevents real harm (dizziness, nausea, migraines) for motion-sensitive users.Animation checklist
- transition for state changes; @keyframes for sequences & loops
- Move with
transform, fade withopacity— nottop/left/width - Durations are short (150–400ms) with a real easing, not
linear - Looping effects use
infinite(+alternatewhere it suits) -
forwardsused when an entrance should hold its end state -
prefers-reduced-motionis respected