CSS animations and transitions
Animate state changes and sequences using compositor-friendly properties and respect for reduced motion.

WHAT YOU WILL BE ABLE TO DO
Learning outcomes
- Explain css animations and transitions in plain language.
- Connect the behavior to the underlying browser or framework model.
- Implement the core pattern and reason through edge cases.
- Answer common follow-ups without relying on memorized phrases.
01Explain it simply
A transition interpolates a property between two values when it changes, such as on hover or a class toggle. An animation uses explicit keyframes to define a sequence over time, independent of a single state change, and can loop, delay, or run without any triggering interaction.
One-line definition: Animate state changes and sequences using compositor-friendly properties and respect for reduced motion.
02Mental model
Prefer animating transform and opacity because the compositor can often handle them without layout or paint, keeping animations smooth even under main-thread load. Animating width, top, or box-shadow forces layout or paint on every frame and is far more likely to drop frames.
03Step by step
- Choose transition for a simple two-state change, animation for a defined sequence.
- Animate transform and opacity wherever the visual result allows it.
- Specify duration and easing deliberately instead of relying on defaults.
- Wrap non-essential motion in a prefers-reduced-motion media query.
- Verify with dev tools that the property runs on the compositor instead of triggering layout.
04Working example
.card { transition: transform 200ms ease-out, opacity 200ms ease-out;}.card:hover { transform: translateY(-4px);}
@media (prefers-reduced-motion: reduce) { .card { transition: none; }}translateY animates on the compositor thread without triggering layout, unlike animating top or margin-top which would. The reduced-motion query removes the transition entirely for users who've asked the OS to minimize motion, without removing the hover state itself.
05Where it is used
- Hover and focus micro-interactions
- Modal and drawer enter-exit transitions
- Loading skeletons and progress indicators
- Page or route transition sequences
06Common mistakes
- Animating width, height, or top for layout changes instead of transform, causing jank
- Ignoring prefers-reduced-motion for large or vestibular-triggering motion
- Leaving will-change applied permanently instead of only around the active animation
- Assuming a CSS transition fires on initial render when it needs an actual property change after mount
07Interview answer
Name the specific property being animated and whether it runs on the compositor — that's the difference between a glib 'use CSS animations for performance' answer and a real one.
Why does animating transform: translateX() typically stay smoother under load than animating left?
left changes an element's position in normal flow, which triggers layout and often paint on every frame; transform can be handled entirely on the compositor thread without recalculating layout, so it keeps animating smoothly even if the main thread is busy.
DDConcept deep dives
Deep dive 1
The animated property decides which pipeline stage runs
transform and opacity can typically be updated on the compositor thread alone, skipping layout and much of paint. Properties that affect box geometry, like width, height, or top, force layout recalculation on every frame, and properties like box-shadow or background force repaint even without a geometry change. The property you choose to animate is the single biggest factor in whether an animation stays smooth under load.
- Prefer transform: translate/scale/rotate over top/left/width/height for motion.
- opacity is compositor-friendly; visibility and display are not animatable the same way.
- A performance trace can confirm which pipeline stages an animation actually triggers.
Deep dive 2
Transitions react to state change; animations define a sequence
A transition has no meaning without something else changing a property's value — it's inherently reactive. A keyframe animation is self-contained: it can run on load, loop indefinitely, or follow a multi-step sequence without any external trigger. Reaching for the wrong one leads to either an animation that never fires, or an over-engineered keyframe set for what was really a simple two-state change.
- A transition needs a value change after the element already has a computed starting value.
- Use animation-iteration-count and animation-direction for looping or alternating sequences a transition can't express.
- Combining both is common: a transition for interactive states, an animation for an entrance sequence.
Deep dive 3
Motion is a feature, not a default
Some users experience discomfort or vestibular symptoms from large-scale motion, and the operating system exposes this as prefers-reduced-motion. Respecting it means removing or shrinking the motion itself while preserving the underlying state change and any feedback the user still needs — an instant fade instead of a sliding parallax, not simply disabling the feature the animation was reinforcing.
- Query prefers-reduced-motion and swap to instant or minimal transitions.
- Keep completion feedback even when the animated flourish is removed.
- Motion-heavy features should always have a design that degrades gracefully.
QAInterview questions and model answers
Attempt each answer aloud before opening it. The model answer shows the depth and precision expected in an interview; it is not a script to memorize.
Intermediate · Coding · 1 min · Question 1When is transition the right tool instead of animation?Open model answer
Model answer
Use transition when a property moves between two states caused by something else changing, such as a class toggle or hover. Use animation when you need an explicit multi-step sequence, looping, or motion that starts without a discrete state change.
/* transition — reacts to a state change */.panel { opacity: 0; transition: opacity 200ms ease; }.panel.open { opacity: 1; }
/* animation — self-starting, multi-step */@keyframes pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.05); } }.badge { animation: pulse 1.5s infinite; }Advanced · Coding · 1 min · Question 2Why are transform and opacity considered compositor-friendly?Open model answer
Model answer
Under common browser optimizations, changes to these properties can often be handled entirely by the compositor thread using an existing layer, skipping layout and much of paint, which keeps the animation smooth even if the main thread is busy.
/* smooth even under main-thread load */.drawer { transition: transform 250ms ease; transform: translateX(-100%); }.drawer.open { transform: translateX(0); }Intermediate · Coding · 1 min · Question 3What does animating width typically force the browser to do on every frame?Open model answer
Model answer
It typically forces layout recalculation because width affects the geometry of the box and potentially its siblings, and that new geometry usually needs to be repainted, which is far more expensive than a compositor-only update.
/* expensive: layout + paint every frame */.bar { transition: width 300ms; }
/* cheaper: compositor only */.bar { transform: scaleX(0); transform-origin: left; transition: transform 300ms; }Intermediate · Coding · 1 min · Question 4How should prefers-reduced-motion be applied without removing functionality?Open model answer
Model answer
Reduce or remove the motion itself — large translations, parallax, spin — while keeping the underlying state change and any necessary feedback, such as an instant opacity swap instead of a sliding transition.
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }}Intermediate · Conceptual · 1 min · Question 5What is a risk of using will-change too broadly?Open model answer
Model answer
Promoting many elements to their own compositor layer consumes GPU memory and can paradoxically slow things down; it should be applied narrowly, close to when the animation starts, and removed afterward.
Open question page →Advanced · Coding · 1 min · Question 6Why doesn't a CSS transition play when a value is set on initial render?Open model answer
Model answer
A transition only interpolates a property that changes value after the element already exists with a computed starting value; if the 'from' and 'to' state are applied in the same paint, there's no change for it to animate between.
/* No animation — element is born at opacity: 1 */.toast { opacity: 1; transition: opacity 200ms; }
/* Fix — mount at 0, then flip on next frame (or use @starting-style) */.toast { opacity: 0; transition: opacity 200ms; }.toast.shown { opacity: 1; }Beginner · Conceptual · 1 min · Question 7What does animation-fill-mode: forwards do?Open model answer
Model answer
It keeps the element in the state defined by the animation's final keyframe after the animation completes, instead of reverting to its pre-animation styles.
Open question page →Advanced · Conceptual · 1 min · Question 8How do you run multiple independent animations on different properties of the same element?Open model answer
Model answer
Comma-separate multiple animation or transition declarations, each with its own property, duration, and timing function, so they run concurrently and independently.
Open question page →Beginner · Conceptual · 1 min · Question 9What is the difference between ease-out and linear timing functions?Open model answer
Model answer
ease-out starts fast and decelerates toward the end, which usually feels more natural for UI motion, while linear moves at a constant rate throughout, which can feel mechanical for organic-feeling interactions.
Open question page →Advanced · Conceptual · 1 min · Question 10Why can animating a pseudo-element be preferable to animating the element itself?Open model answer
Model answer
It can isolate the animated paint or compositing work to a smaller, dedicated layer without affecting the main element's own box, sometimes avoiding unnecessary repaint of unrelated content.
Open question page →SCScenario questions
Scenario 1
A card list's expand animation drops frames on lower-end devices because it animates max-height and box-shadow together.
- Identify which animated properties trigger layout versus paint versus compositing.
- Replace max-height animation with a transform-based reveal where the design allows it.
- Move box-shadow to a pseudo-element or accept a cheaper visual approximation.
- Measure the result with a performance trace on representative hardware.
Reveal worked answer
max-height animation forces layout on every frame, and animating box-shadow forces repaint. I would restructure the reveal to animate a transform (such as scaleY with transform-origin, or translateY combined with a fixed max height) and opacity, and either drop the animated shadow or fake it with a cheaper technique, then confirm smoothness in a trace on lower-end hardware rather than just a fast dev machine.