036: Transforms, Transitions, and Keyframe Animations
Learning outcomes
By the end, you can transform elements without confusing visual movement with layout position; build transitions that target only the properties you intend to change; reason about easing and duration; create keyframe animations; choose properties that animate well; respect prefers-reduced-motion; and debug motion that creates layout, paint, performance, or accessibility problems.
Prerequisites and retrieval
Retrieve positioning from 026 and interaction states from 033. The useful starting point is a usable static interface: motion should clarify or support that interface, not become a requirement for understanding it.
Transforms
A transform changes how an element is rendered by changing its coordinate system. It does not, by itself, ask the layout engine to find new positions for neighboring boxes.
.card {
transform: translateY(-4px);
}
The example renders the card four pixels higher than its untransformed position. That makes transforms useful for visual effects such as a small lift on hover, rotation, scaling, or decorative movement.
Common functions include:
transform: translate(1rem, -0.5rem);
transform: scale(1.05);
transform: rotate(3deg);
transform: skewX(-5deg);
Multiple functions can be composed in one declaration:
.icon {
transform: translateX(0.25rem) rotate(8deg);
}
The order matters because the functions are applied in sequence. Changing the order can change the coordinate system used by the next function, so inspect the complete transform rather than treating the values as an unordered list.
Transform origin
By default, transforms use the element's center as their origin. transform-origin lets you choose the point around which rotation or scaling occurs.
.menu-icon {
transform-origin: center;
}
For example, scaling from the left edge makes a badge grow toward the right instead of growing equally in both directions:
.badge {
transform-origin: left center;
transform: scale(1.1);
}
Important layout distinction
This distinction is one of the places where CSS motion causes avoidable confusion. A transformed element can appear in a different location while the layout still reserves the element's original space.
.card {
transform: translateY(-20px);
}
The card is visually moved, but surrounding layout still reserves its original position. If other boxes need to reflow, change layout properties instead of using transform as a layout hack. A transform is the right tool for visual movement; it is not a replacement for grid, flexbox, margins, or positioning when the layout itself must change.
Transitions
A transition interpolates between a property's old and new values when a state change occurs. Here, hovering the button changes its background, and the transition controls how that change is rendered over time.
.button {
background: #2563eb;
transition: background-color 160ms ease;
}
.button:hover {
background: #1d4ed8;
}
The transition belongs on the base rule, not only on :hover, so the button also transitions smoothly when the pointer leaves. It is still the state rule that supplies the new value.
The longhand properties expose the individual controls. When several properties are listed, the comma-separated values correspond by position:
.button {
transition-property: background-color, transform;
transition-duration: 160ms, 160ms;
transition-timing-function: ease, ease;
transition-delay: 0ms, 0ms;
}
Prefer a list of intended properties over this broad declaration:
transition: all 200ms ease;
all can animate properties you did not intend to animate after a later CSS change. Explicit properties make the interaction easier to review and keep unrelated changes immediate.
Easing
The timing function controls the rate of change during the transition. Common timing functions are:
transition-timing-function: linear;
transition-timing-function: ease;
transition-timing-function: ease-in;
transition-timing-function: ease-out;
transition-timing-function: cubic-bezier(.2, .8, .2, 1);
Use easing to communicate the intent of the interaction. Linear motion has a constant rate, while the built-in easing functions vary that rate. Tiny interface feedback often feels good with a short ease-out, because the response arrives quickly and settles without making the control feel sluggish. Duration and easing work together; changing one can alter the perceived weight of the same movement.
Worked example: button interaction
This example coordinates the visual properties that change across the button's interaction states. The motion is small enough to acknowledge the interaction without delaying it.
.button {
transform: translateY(0);
background: #2563eb;
box-shadow: 0 2px 4px rgb(15 23 42 / 0.12);
transition:
transform 140ms ease-out,
background-color 140ms ease-out,
box-shadow 140ms ease-out;
}
.button:hover {
background: #1d4ed8;
transform: translateY(-1px);
box-shadow: 0 5px 14px rgb(15 23 42 / 0.16);
}
.button:active {
transform: translateY(0);
}
.button:focus-visible {
outline: 3px solid #f59e0b;
outline-offset: 3px;
}
The focus indicator is not replaced by motion. A keyboard user must still have a clear, non-motion indication of focus, and :focus-visible keeps that indication tied to the appropriate interaction state.
Keyframe animations
Transitions describe movement between states. A keyframe animation is useful when the motion needs several defined stages or needs to run independently of a single state change.
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.04);
}
100% {
transform: scale(1);
}
}
.status-dot {
animation: pulse 1.2s ease-in-out infinite;
}
The @keyframes block names the animation and describes its values at selected points. The animation declaration then chooses how pulse runs: its duration, easing, and infinite iteration are all explicit.
The shorthand can also include the animation's other controls:
animation:
pulse
1.2s
ease-in-out
0s
infinite
normal
both;
Important subproperties include:
animation-nameanimation-durationanimation-timing-functionanimation-delayanimation-iteration-countanimation-directionanimation-fill-modeanimation-play-state
Learn to read the shorthand as these separate decisions. When an animation behaves unexpectedly, expanding it into subproperties often makes the missing or incorrect value obvious.
Worked example: enter animation that remains optional
An enter animation can provide a little context as content appears, but the content must work when the animation is unavailable or intentionally disabled.
@keyframes fade-rise {
from {
opacity: 0;
transform: translateY(0.5rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.notice {
animation: fade-rise 220ms ease-out both;
}
The both fill mode applies the first and last keyframe outside the active interval. That prevents the notice from briefly appearing in its unanimated starting state and ensures it remains at its final state after the animation completes.
Reduced motion removes the animation:
@media (prefers-reduced-motion: reduce) {
.notice {
animation: none;
}
}
The content is immediately visible without animation. Removing the motion must not remove the notice or make its state harder to understand.
Motion and performance
Transforms and opacity are often efficient choices for visual motion because they may avoid repeated layout, but this is not an absolute guarantee. Browser rendering depends on context, the rest of the page, the device, and the amount of work each frame requires.
Avoid animating layout-heavy properties such as width, height, or top/left on large complex regions when a transform can express equivalent decorative motion.
Bad pattern:
.drawer {
left: -20rem;
transition: left 300ms;
}
Often better:
.drawer {
transform: translateX(-100%);
transition: transform 300ms ease;
}
The second version expresses the drawer's visual travel without repeatedly changing its layout position. It is still a choice to validate, not a performance guarantee. Test actual performance and accessibility, including whether the drawer remains discoverable and usable with the motion reduced.
Reduced motion
The prefers-reduced-motion media feature exposes a user preference that should influence optional movement. A component can remove decorative motion while retaining the state and information it communicates.
@media (prefers-reduced-motion: reduce) {
.parallax,
.auto-rotating-carousel,
.decorative-spinner {
animation: none;
transform: none;
}
}
Do not remove an essential progress indicator without replacing its meaning. Reduced motion means reducing unnecessary movement, not hiding system status. If the moving indicator is the only signal that work is in progress, provide a textual or otherwise accessible status as well.
Worked example: loading indicator with accessible status
The spinner below is decorative because the surrounding element exposes the status text through role="status". The text remains useful whether the spinner animates, is paused, or is not rendered.
<div class="loading" role="status">
<span class="spinner" aria-hidden="true"></span>
Loading orders…
</div>
.spinner {
inline-size: 1rem;
aspect-ratio: 1;
border: 2px solid #cbd5e1;
border-block-start-color: #2563eb;
border-radius: 50%;
animation: spin 700ms linear infinite;
}
@keyframes spin {
to {
transform: rotate(1turn);
}
}
@media (prefers-reduced-motion: reduce) {
.spinner {
animation: none;
border-block-start-color: #2563eb;
}
}
The textual status remains regardless of motion. Keeping that status in the markup also makes the component's meaning available to users who cannot see the animation.
Deep dive: animation pipeline and compositing
Not every animated property has the same cost. To debug a slow animation, distinguish the work involved in changing layout from the work involved in painting and compositing the result.
Changes to layout properties can trigger layout and paint:
/* often more expensive for motion */
.panel {
transition: inline-size 300ms;
}
Transforms and opacity are often easier for browsers to composite efficiently:
.panel {
transition:
transform 180ms ease,
opacity 180ms ease;
}
This is a performance guideline, not a law. Measure real pages instead of assuming every transform is free. A complicated layer, expensive paint, or too much simultaneous motion can still produce poor frames.
3D transforms
.scene {
perspective: 800px;
}
.card {
transform: rotateY(12deg) translateZ(20px);
transform-style: preserve-3d;
}
3D transforms change coordinate systems and stacking behavior. They can also make text, controls, and pointer targets harder to interpret. Use them sparingly for interfaces; readability and pointer targets are more important than spectacle.
Transition only what you intend
The broad form is tempting because it appears to make every state change smooth:
.button {
transition: all 200ms ease;
}
all can accidentally animate a future property that should change immediately. That can create surprising interactions and makes it harder to identify which declaration controls the motion.
Prefer an explicit list:
.button {
transition:
background-color 160ms ease,
color 160ms ease,
transform 120ms ease;
}
Now the intended visual changes are visible in the rule, and a newly added property will not silently inherit an animation.
Entry/exit transitions and discrete properties
Modern CSS can transition some previously awkward entry and exit cases with @starting-style and transition-behavior: allow-discrete. These features are useful for components such as popovers, but they do not remove the need for a usable resting state.
.popover-panel {
opacity: 1;
transform: translateY(0);
transition:
opacity 160ms,
transform 160ms,
display 160ms allow-discrete;
}
@starting-style {
.popover-panel:popover-open {
opacity: 0;
transform: translateY(-0.4rem);
}
}
Support and exact behavior should be checked against the project's browser baseline. Build a usable non-animated state first; the transition is an enhancement to that state, not the mechanism that makes the component understandable.
Keyframe control
Keyframes can describe a repeating state change with only the values that matter:
@keyframes pulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
.sync-indicator {
animation:
pulse 1.2s ease-in-out infinite;
}
Important controls include:
animation-duration;animation-delay;animation-iteration-count;animation-direction;animation-fill-mode;animation-play-state;animation-timing-function.
These controls determine whether the animation starts immediately, repeats, reverses, holds a keyframe, pauses, or changes speed. Do not use infinite animation for decoration that continuously demands attention. A loop should communicate ongoing activity or state, not compete with the content.
Reduced motion is a design branch
Reduced motion is not just a final override for one spinner. It is a design branch in which each component preserves its state change while choosing less movement.
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto;
}
.sync-indicator {
animation: none;
}
.panel {
transition-duration: 0.01ms;
}
}
A blanket reset can be useful in controlled projects, but component-specific alternatives are often better. Some users tolerate fades but not large movement. Preserve the state change even when motion is removed: a panel still needs to open, a status still needs to update, and a process still needs to expose its progress.
Motion debugging
When a transition does not run, check the conditions that must exist for interpolation rather than repeatedly increasing the duration:
- Confirm the property has a previous computed value.
- Confirm the before and after values are animatable.
- Check whether the element was just inserted or changed from
display: none. - Inspect
transition-property. - Check reduced-motion rules.
- Inspect whether JavaScript changes state twice in the same rendering frame.
Understanding the lifecycle is more reliable than repeatedly increasing duration. If the browser never observes two usable states, a longer duration cannot create a transition. If it does observe them but the result is still slow or janky, inspect the rendering work rather than the timing function alone.
Common mistakes
Watch for these failure modes when reviewing motion:
transition: all.- Animating every hoverable component.
- Long entrance animations that delay access to content.
- Parallax or large continuous movement without reduced-motion handling.
- Using transform for actual page layout.
- Forgetting animation can create stacking contexts.
- Infinite animation drawing attention to non-essential decoration.
- Relying on movement as the only status change.
Practice set
Work through these in order. The first exercise establishes explicit transition control; the later exercises add layout comparison, animation lifecycle, accessibility, and observation in DevTools.
- Build a button with targeted transition properties.
- Compare transform movement with
position: relative; top. - Create a one-time notification enter animation.
- Add a reduced-motion alternative.
- Build a spinner with visible text status.
- Use DevTools animation tools to inspect duration and easing.
Recap
Transforms change rendered geometry without automatically changing surrounding layout. Transitions interpolate between property states when a state changes, while keyframes describe multi-step or independently running animations. Motion is strongest when it is brief, purposeful, optional, and compatible with user preferences. When it misbehaves, inspect the property lifecycle and rendering cost instead of treating duration as the only control.
Official references
-
MDN:
@starting-style— https://developer.mozilla.org/en-US/docs/Web/CSS/@starting-style -
MDN: Using CSS transitions — https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_transitions/Using_CSS_transitions
-
MDN: Transform — https://developer.mozilla.org/en-US/docs/Web/CSS/transform
-
MDN: CSS transitions — https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_transitions
-
MDN: CSS animations — https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animations
-
MDN:
prefers-reduced-motion— https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion
