Home / Articles / Animation & Motion /
Performance: which properties are safe to transition?
The difference between a smooth 60fps animation and a janky one usually comes down to which property you chose to animate. Here's how to pick the right ones.
The rendering pipeline
When a CSS property changes, the browser runs through up to three stages: Layout, Paint, and Composite. The earlier in the pipeline a property triggers work, the more expensive it is to animate.
- Layout — recalculates geometry (position, size) of the element and its neighbors. Triggers paint and composite too.
- Paint — redraws pixels (colors, shadows, borders). Triggers composite.
- Composite — rearranges already-painted layers on the GPU. Cheapest step.
The goal is to animate only composite properties whenever possible.
The safe list: composite-only properties
These properties can be animated entirely on the GPU compositor thread, meaning the main thread stays free for other work.
/* Composite-only — always safe for 60fps */
.element {
transition:
transform 0.3s ease,
opacity 0.3s ease,
filter 0.3s ease;
}
/* All movement via transform */
.card:hover {
transform: translateY(-4px) scale(1.02);
opacity: 0.95;
filter: brightness(1.1);
}
transform, opacity, and filter are the three properties you can always animate without worry. They skip layout and paint entirely.The caution list: paint-triggering properties
These properties skip layout but still require the browser to repaint pixels. They're acceptable for small elements or short durations, but can cause jank on large areas.
/* Paint-only — moderate cost */
.btn {
background: oklch(0.52 0.22 265);
color: oklch(1 0 0);
box-shadow: 0 2px 8px oklch(0% 0 0 / 0.12);
transition:
background 0.2s ease,
color 0.2s ease,
box-shadow 0.25s ease;
}
.btn:hover {
background: oklch(0.48 0.24 265);
color: oklch(0.98 0.01 260);
box-shadow: 0 8px 24px oklch(0% 0 0 / 0.2);
}
Properties in this tier include background-color, color, border-color, box-shadow, and outline. They're fine for buttons and small components — avoid them on full-screen elements.
The avoid list: layout-triggering properties
These properties force the browser to recalculate the position and size of multiple elements. Animating them causes reflow — the most expensive operation.
/* AVOID — these trigger layout on every frame */
.panel {
transition:
width 0.3s ease, /* layout */
height 0.3s ease, /* layout */
padding 0.3s ease, /* layout */
margin 0.3s ease, /* layout */
top 0.3s ease, /* layout */
left 0.3s ease; /* layout */
}
/* PREFER — use transform equivalents */
.panel {
transition: transform 0.3s ease;
}
.panel:hover {
/* Instead of width/height, use scale */
transform: scale(1.05);
/* Instead of top/left, use translate */
/* transform: translate(10px, -5px); */
}
The layout-triggering properties include width, height, padding, margin, top, right, bottom, left, border-width, and font-size.
will-change: use sparingly
will-change hints to the browser that a property will animate soon, allowing it to promote the element to its own compositor layer in advance.
/* Apply only when animation is imminent */
.card:hover {
will-change: transform;
}
/* Or toggle via a class before animation starts */
.card.will-animate {
will-change: transform, opacity;
}
/* AVOID — permanent will-change wastes GPU memory */
.card {
will-change: transform; /* Don't do this globally */
}
Each will-change element gets its own GPU layer, consuming video memory. On a page with hundreds of cards, permanent will-change can crash mobile browsers.
will-change right before the animation — on :hover, or via JavaScript just before adding an animation class. Remove it afterward.Measuring with DevTools
Don't guess which properties are expensive — measure. Chrome DevTools provides direct visibility into rendering costs.
- Performance panel: Record an interaction and look for long frames (red bars). Expand "Rendering" to see layout and paint times.
- Layers panel: Shows which elements have their own compositor layers and why. Elements with
transformorwill-changeget promoted. - Rendering tab: Enable "Paint flashing" to see green overlays on repainted areas. If your entire page flashes green during an animation, you're animating a paint property on a large element.
/* If DevTools shows layout thrashing, refactor like this: */
/* Before — animating height triggers layout */
.drawer {
height: 0;
transition: height 0.3s ease;
}
.drawer.open { height: 200px; }
/* After — animating transform skips layout */
.drawer {
transform: scaleY(0);
transform-origin: top;
transition: transform 0.3s ease;
}
.drawer.open { transform: scaleY(1); }
Quick reference table
Use this as a cheat sheet when choosing which properties to animate.
- Composite (safe):
transform,opacity,filter,backdrop-filter,clip-path(in some browsers) - Paint (moderate):
background,color,box-shadow,border-color,outline,text-decoration-color - Layout (avoid):
width,height,padding,margin,top/right/bottom/left,border-width,font-size,gap
When in doubt, animate transform and opacity. They cover the vast majority of UI motion — movement, scaling, rotation, and fading — all on the compositor.