CSS animations
Choose transitions for state changes
A CSS transition interpolates a property from its current value to a new value over time. The browser supplies the values between those two states. A hover change is the standard case:
.button {
opacity: 0.7;
transform: translateY(0);
transition: opacity 160ms ease, transform 160ms ease;
}
.button:hover,
.button:focus-visible {
opacity: 1;
transform: translateY(-2px);
}
List the properties you intend to animate. Other changed properties then update immediately.
Wrong "Every animation needs @keyframes."
Right A transition fits a change between a current state and an end state. Use keyframes when you need to describe intermediate stages or run a sequence without a state change such as hover.
Use keyframes for a sequence
Keyframe animations separate the sequence from the element that runs it. The animation properties choose the duration, easing, iteration count, direction, and other timing details.
@keyframes arrive {
from {
opacity: 0;
transform: translateY(12px);
}
70% {
opacity: 1;
transform: translateY(-2px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.notice {
animation: arrive 240ms ease-out;
}
The browser interpolates from one keyframe to the next. A keyframe at 70% makes the small overshoot part of the sequence. A two-state transition has no comparable author-defined waypoint.
Transform moves the drawing after layout
translate(), scale(), and rotate() are transform functions. A transformed element keeps its original place in normal layout, so nearby elements do not move to make room for its translated drawing.
.card:hover {
transform: scale(1.03);
}
opacity changes how opaque the element is. Both transform and opacity are commonly suitable for motion because browsers can handle them during composition when the element is rendered in its own layer. This is an optimization path, not a promise that every transform runs on a GPU.
Wrong "Animating left and animating translateX() are equivalent because both move pixels."
Right left affects the positioned element's geometry and can trigger layout and paint. A transform can avoid those stages when composited.
Respect a user's request to reduce non-essential motion:
@media (prefers-reduced-motion: reduce) {
.button,
.notice {
animation: none;
transition: none;
}
}
Read the rendering work behind each frame
The useful performance distinction is the property being animated. Choosing CSS instead of JavaScript does not turn an expensive property into a cheap one.
When a changed property affects geometry or position, the browser may have to recalculate styles, lay out boxes, and paint pixels. MDN lists left, margin-left, max-width, border-width, and font-size in this category. A paint-only change such as color can skip layout but still repaint. Layers are combined during composition after painting.
For an element rendered in its own layer, changing transform or opacity can be handled during composition. Existing pixels can be moved or blended without laying out and repainting the element on every frame.
Here is a drawer that makes layout part of its animation:
.drawer {
position: fixed;
width: 20rem;
left: -20rem;
transition: left 220ms ease;
}
.drawer[data-open="true"] {
left: 0;
}
Keep its layout position stable and express the visual movement as a transform:
.drawer {
position: fixed;
width: 20rem;
left: 0;
transform: translateX(-100%);
transition: transform 220ms ease;
}
.drawer[data-open="true"] {
transform: translateX(0);
}
Wrong "transform and opacity never touch the main thread."
Right They can avoid layout and paint when the browser renders the element in its own layer. Style recalculation may still occur, and layer selection is an implementation decision. Record the animation in browser performance tools to see which stages ran.
will-change is a targeted hint
will-change tells the browser which property is expected to change. The browser may prepare an optimization before the change begins. It does not define an animation, force GPU execution, or remove the layout cost of a geometry-changing property.
MDN describes will-change as a last resort for an existing performance problem. Too many prepared elements consume resources and can make the page slower. A value such as will-change: opacity can also create a stacking context before the opacity changes.
If measurement shows a promotion delay on a frequently used control, apply the hint shortly before the animation and remove it afterward:
const drawer = document.querySelector(".drawer");
drawer.addEventListener("pointerenter", () => {
drawer.style.willChange = "transform";
});
drawer.addEventListener("transitionend", () => {
drawer.style.willChange = "auto";
});
Wrong "Put will-change: transform on every component that might animate."
Right Measure first, use the hint sparingly, give the browser time to prepare, and remove the hint when the change is over.
The browser already applies its own optimization heuristics. A permanent site-wide will-change rule prevents those optimizations from being released as quickly as they otherwise could be.
Web Animations adds runtime control
Element.animate(keyframes, options) is a shortcut that creates an Animation, applies it to the element, starts playback, and returns that Animation. The keyframes may be an array of objects or an object whose property values are arrays.
const panel = document.querySelector(".panel");
const animation = panel.animate(
[
{ opacity: 0, transform: "translateY(12px)" },
{ opacity: 1, transform: "translateY(0)" },
],
{
duration: 240,
easing: "ease-out",
fill: "both",
},
);
pauseButton.addEventListener("click", () => animation.pause());
reverseButton.addEventListener("click", () => animation.reverse());
CSS keyframes remain a good fit when CSS owns the sequence. Retain the returned Animation when application logic needs playback control or synchronization. This API choice is separate from rendering cost. Animating left through Web Animations can still require layout and paint.
Wrong "Web Animations is faster because JavaScript sends every property to the compositor."
Right The API supplies an animation model and playback controls. The animated property and the browser's layer decisions determine which rendering stages can be skipped.
View transitions animate a UI replacement
A same-document view transition coordinates a DOM update with captured views. The browser captures the old view, invokes the update callback, captures the new view, then animates the old and new snapshots. The default is a cross-fade for most appearance changes.
The DOM update must still happen when the API is unavailable:
function showRoute(route) {
const update = () => renderRoute(route);
if (!document.startViewTransition) {
update();
return;
}
document.startViewTransition(update);
}
The old snapshot is static. The new snapshot is a live DOM region. The ViewTransition.finished promise fulfills when the transition reaches its end state, including when the animation is skipped. For a same-document transition, it rejects if the update callback throws or returns a rejected promise. Give an element a view-transition-name when it should animate separately from the root view:
.product-hero {
view-transition-name: product-hero;
}
::view-transition-old(product-hero),
::view-transition-new(product-hero) {
animation-duration: 320ms;
}
Wrong "document.startViewTransition() performs the route change."
Right It runs the callback supplied by the application. That callback changes the DOM. Feature detection must fall back to running the same update directly.
Cross-document transitions use CSS opt-in
As documented in July 2026, a transition between documents requires both documents to opt in and the navigation to be same-origin. The navigation triggers the view update, so the basic case requires no JavaScript:
@view-transition {
navigation: auto;
}
View transition pseudo-elements expose the captured states. ::view-transition-old(root) targets the old static snapshot and ::view-transition-new(root) targets the new live snapshot. Custom CSS animations can replace the default cross-fade:
@keyframes leave {
to {
opacity: 0;
transform: translateY(-1rem);
}
}
::view-transition-old(root) {
animation: leave 180ms ease-in both;
}
Snapshot animation solves a different problem from an ordinary transition on a persistent element. It gives the browser old and new visual states around a DOM replacement or same-origin navigation, while the application remains responsible for the state change itself.
dig deeper
Primary sources behind these notes - the specs and official docs worth reading in full.
beta
The interviewer part is in the works.
The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.