Animating with transform and opacity

For smooth, high-performance CSS animations, keep your animated properties restricted to transform and opacity. These two properties are handled at the compositor stage of the rendering pipeline, which means the browser can update them without redoing layout or paint work. Every other property risks triggering the more expensive parts of the rendering process.

Moving elements

Use the translate and rotate keyword values of the transform property for movement. The translate value slides an element from its current position; the rotate value spins it around its origin.

To slide an item into view:

.animate {
  animation: slide-in 0.7s both;
}

@keyframes slide-in {
  0% {
    transform: translateY(-1000px);
  }
  100% {
    transform: translateY(0);
  }
}

For a full 360-degree rotation:

.animate {
  animation: rotate 0.7s ease-in-out both;
}

@keyframes rotate {
  0% {
    transform: rotate(0);
  }
  100% {
    transform: rotate(360deg);
  }
}

Scaling elements

Resize elements with the scale keyword of transform:

.animate {
  animation: scale 1.5s both;
}

@keyframes scale {
  50% {
    transform: scale(0.5);
  }
  100% {
    transform: scale(1);
  }
}

Showing and hiding elements

The opacity property controls visibility. Fading an element in or out is a compositor-friendly operation:

.animate {
  animation: opacity 2.5s both;
}

@keyframes opacity {
  0% {
    opacity: 1;
  }
  50% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

What to avoid

Before animating any CSS property other than transform or opacity, check how it affects the rendering pipeline. Animating top, left, width, height, or margin, for example, forces the browser to recalculate layout on every frame. Triggering paint is also expensive—especially for effects like shadows or blurs, which cost far more to render than simple colored boxes.

Compare these two approaches for moving an element:

Don't:

.box {
  position: absolute;
  top: 10px;
  left: 10px;
  animation: move 3s ease infinite;
}

@keyframes move {
  50% {
     top: calc(90vh - 160px);
     left: calc(90vw - 200px);
  }
}

Do:

.box {
  position: absolute;
  top: 10px;
  left: 10px;
  animation: move 3s ease infinite;
}

@keyframes move {
  50% {
     transform: translate(calc(90vw - 200px), calc(90vh - 160px));
  }
}

The transform-based version runs at the composite stage, while the top/left version forces layout work each frame.

Managing layers

Putting an animated element on its own layer lets the browser repaint it independently without repainting everything else on the page. Browsers usually decide this on their own, but you can force a layer with the will-change property:

body > .sidebar {
  will-change: transform;
}

Apply will-change sparingly. According to the CSS specification, reserve it for elements that are about to change—like a sidebar that slides in and out. For elements that change infrequently, set will-change from JavaScript right before the change is expected, and remove it once the change finishes. This gives the browser time to optimize without permanently reserving memory.

If you need to support browsers without will-change, you can force layer creation with transform: translateZ(0).

Debugging slow animations

Chrome and Firefox DevTools both offer ways to identify why an animation stutters.

Check for layout triggers

Chrome DevTools

  1. Open the Performance panel.
  2. Record runtime performance while the animation runs.
  3. Check the Summary tab.

A nonzero value for Rendering means the animation is making the browser do layout work. Animation performance snapshots show the difference clearly between layout-driven and compositor-driven animations.

Firefox DevTools

In the Performance panel, the Waterfall tab shows where the browser spends its time. If you see Recalculate Style entries, the animation has to go back to the start of the rendering waterfall on each frame.

Check for dropped frames

Chrome's FPS meter helps here:

  1. Open the Rendering tab in Chrome DevTools.
  2. Enable the FPS meter checkbox.
  3. Watch the values while the animation plays.

The Frames label at the top shows a readout like 50% 1 (938 m) dropped of 1878. A high-performance animation should show a high percentage—around 99%—meaning very few frames drop and motion stays smooth.

Check for paint work

Not all paint operations cost the same. Shadows and blurs are much more expensive than flat fills. DevTools can highlight exactly which regions the browser repaints.

Chrome DevTools

  1. Open the Rendering tab.
  2. Turn on Paint Flashing.
  3. Move the pointer across the screen.

If the entire screen flashes, or areas you didn't expect light up, investigate further. For deeper analysis of whether a specific property drives painting cost, use Chrome's paint profiler.

Firefox DevTools

  1. Open Settings and add a Toolbox button for Toggle paint flashing.
  2. On the page under test, enable the toggle and move the mouse or scroll.

If the paint profiler reveals an especially expensive operation, look for a different CSS property that achieves the same visual result without the cost.

Summary of best practices

  • Animate only transform and opacity to stay at the composition stage.
  • For movement, use translate and rotate; for resizing, use scale; for visibility, use opacity.
  • Avoid properties that trigger layout or paint unless there's no alternative.
  • Use will-change to force layer creation only when you actually hit a performance problem and the element genuinely changes.