Which CSS properties should you avoid animating, and why prefer transform/opacity
Avoid animating layout properties (width, height, top, left, margin, padding) — they trigger reflow + paint every frame on the main thread. Avoid paint-heavy ones (box-shadow, background, border-radius) when possible. Prefer `transform` and `opacity`: they're compositor-only, GPU-accelerated, and run off the main thread, so they stay at 60fps.
Not all CSS properties are equal to animate. The cost depends on which stage of the rendering pipeline they trigger: Style → Layout → Paint → Composite.
Avoid: layout-triggering properties
width, height, top, right, bottom, left, margin, padding, border-width, font-size...
Animating these forces Layout (reflow) every frame — the browser recalculates geometry, often for siblings and children too — then Paint, then Composite. It all runs on the main thread, so it competes with your JS and easily misses the ~16ms frame budget → jank.
Avoid where possible: paint-heavy properties
color, background-color, box-shadow, border-radius, background-image...
These skip Layout but still force Paint every frame. Cheaper than layout, but still not free. A common trick: instead of animating box-shadow, put the shadow on a pseudo-element and animate its opacity.
Prefer: transform and opacity
transform (translate, scale, rotate, skew) and opacity are compositor-only:
- No Layout, no Paint.
- The element gets its own GPU layer; the compositor just re-positions/re-blends it.
- Can run on the compositor thread, off the main thread — smooth even while JS is busy.
/* ❌ avoid */
.menu { transition: left 300ms, height 300ms; }
/* ✅ prefer */
.menu { transition: transform 300ms, opacity 300ms; }| Want to animate… | Don't | Do |
|---|---|---|
| Move something | top / left / margin | transform: translate() |
| Resize something | width / height | transform: scale() |
| Show / hide | display / visibility | opacity (+ visibility at the end) |
| Reorder / shift list | animate top | the FLIP technique with transform |
When you genuinely must animate layout
- Use the FLIP technique: measure First and Last positions, apply an inverting
transform, then animate the transform to zero — you get a layout result with a compositor animation. - Animate only on a small, isolated element.
- Consider
content-visibility/containto limit layout scope.
will-change — the careful tool
will-change: transform pre-promotes an element to its own layer so the first frame isn't janky. But each layer costs GPU memory — use it on the few elements that animate, remove it when done. It's a scalpel, not a default.
Senior framing
The senior framing: "cheap to animate" = "doesn't reach Layout or Paint." transform/opacity are the safe set; everything else costs main-thread work per frame. Knowing the FLIP pattern for when you do need a layout change, and that will-change has a memory cost, separates a real answer from "just use transform."
Follow-up questions
- •What is the FLIP animation technique?
- •How would you animate a box-shadow performantly?
- •Why is animating `display` impossible/janky?
Common mistakes
- •Animating width/height/top/left and blaming the device for jank.
- •Animating box-shadow directly instead of an opacity proxy.
- •Trying to transition `display: none` — it's not animatable.
- •Leaving will-change on permanently.
Edge cases
- •`visibility` IS transitionable in a stepwise way; `display` is not (until newer @starting-style support).
- •Transform-based scale can distort children unless you counter-scale.
- •Sub-pixel transforms can cause text blur on some GPUs.
Real-world examples
- •Drawer/menu slide-ins, modal fades, list reordering with FLIP, hover effects.