Module 7: Visual Effects, Transitions & Animations
Transitions: Animating Between States
How a transition decides when to run, the timing functions, what can and cannot be animated, and why transform and opacity are the cheap ones.
A transition smooths the change of a property from one value to another. Instead of a button turning blue instantly on hover, it fades over 200 milliseconds. You do not write the in-between frames. You describe how long it should take, and the browser interpolates.
The four properties#
.btn {
background: #0b5fff;
transition-property: background-color, transform;
transition-duration: 200ms;
transition-timing-function: ease-out;
transition-delay: 0s;
}
.btn:hover {
background: #0847c4;
transform: translateY(-2px);
}
In practice you write the shorthand:
.btn {
transition: background-color 200ms ease-out, transform 200ms ease-out;
}
The order is: property, duration, timing function, delay. The first time value is always the duration. The second is the delay.
When does a transition start?#
A transition runs when the computed value of a property changes on an element that is already rendered. Hover, focus, a class toggled by JavaScript, a media query change: anything that changes the computed value triggers it.
The transition declaration belongs on the base state, not the hover state:
.btn { transition: background-color 200ms; } /* applies in both directions */
.btn:hover { background-color: #0847c4; }
If you put it on :hover only, the transition plays when you enter the hover state, but on leaving, the hover rule no longer applies, so there is no transition and the color snaps back.
It also means an element's first render does not transition. The browser has no "before" value. To animate something in on page load, you need either a keyframe animation (next lessons), or @starting-style in newer browsers.
Timing functions#
The timing function is the acceleration curve of the change.
linear ────────────── constant speed
ease slow→fast→slow (default: gentle both ends)
ease-in slow start, fast end (good for things leaving)
ease-out fast start, slow end (good for things entering and UI feedback)
ease-in-out slow both ends
.a { transition-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1); } /* custom curve */
.b { transition-timing-function: steps(4, end); } /* jumps in 4 stages */
For interface feedback, ease-out feels responsive because the movement begins quickly. Use linear for things like a progress bar or color cycling, where acceleration would look wrong.
Duration matters more than the curve. 100 to 200ms feels instant. 200 to 300ms is noticeable and smooth. Above about 500ms, interface changes start to feel slow.
What can be transitioned#
Only properties with values the browser can interpolate between: numbers, lengths, percentages, colors, transforms, shadows. These work:
opacity, color, background-color, width, height, margin, padding, transform, box-shadow, border-radius, filter.
These do not (or not by default):
display: it is a switch, with no in-between values.height: auto: the browser cannot interpolate from0to the keywordauto.background-imageswapped from oneurl()to another.
Collapsible sections without height: auto#
A widely used workaround animates a grid row, because 0fr to 1fr is interpolatable:
.panel {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 250ms ease;
}
.panel.open { grid-template-rows: 1fr; }
.panel > div { overflow: hidden; } /* the inner element gets squeezed */
Newer browsers also support interpolate-size: allow-keywords, which lets height: auto animate directly, and transition-behavior: allow-discrete with @starting-style, which lets you fade an element in and out of display: none. They are supported in recent versions, and you should check compatibility before relying on them.
What each property costs#
Remember the pipeline from Module 1. Transitioning width forces layout every frame, which is expensive when the page is complex. transform and opacity can be handled by the compositor, usually on the GPU, without redoing layout or paint.
/* Costly: recalculates layout on every frame */
.card:hover { margin-top: -4px; }
/* Cheap: composite-only */
.card:hover { transform: translateY(-4px); }
If you want to move something, use transform. If you want to fade something, use opacity. You can use other properties without issue when elements are simple and the change is occasional. Reserve the discipline for large, frequent or scroll-driven effects.
Watch out for transition: all. It animates every property that changes, including ones you did not intend, such as when a media query changes width or a class alters padding, and every change becomes a layout animation. List the properties you mean.
Respecting motion preferences#
Some users get dizzy or distracted by motion and ask the operating system to reduce it. Honor that:
@media (prefers-reduced-motion: reduce) {
.btn { transition: none; }
}
Subtle fades are usually fine. Large movements, parallax and spinning are what you should remove.
Common mistakes#
- Putting
transitionon the hover rule and getting no animation on the way back. transition: all, which animates things you never intended.- Trying to transition
displayorheight: auto. - Animating
top,left,widthandheightwhen a transform would do. - Durations that are too long. A 1-second hover effect feels broken.
- Expecting transitions on first render.
- Forgetting that
visibilitychanges happen at an odd moment unless you transition it too.
Practice#
- Build a button with background, shadow and lift transitions. Confirm all three animate in both directions.
- Reproduce the "transition on hover only" bug on purpose, then fix it.
- Create an accordion panel that opens smoothly using the
grid-template-rowstechnique. - Move a box 100px by animating
left, then again withtranslateX. Use the DevTools Performance panel to compare the work done, and add a reduced-motion rule.