Layout Shift caused by CSS transitions

Learn how to find and remove CSS transitions that create layout shifts

Arjen Karel Core Web Vitals Consultant
Arjen Karel - linkedin
Last update: 2026-03-02

Why CSS transitions cause layout shifts

Cumulative Layout Shifts caused by CSS transitions often occur early during the loading phase of the page. These layout shifts do not happen consistently, which makes them hard to debug.

Last reviewed by Arjen Karel on March 2026

The transition: all problem

The most common cause of transition-related layout shifts is transition: all. According to the 2022 Web Almanac's CSS chapter, 53% of pages use transition: all. That means every property change gets animated, including layout-affecting ones like width, height, margin, and padding.

A CSS transition has a property, duration, timing-function and a delay. The shorthand looks like this:

/* property | duration | timing-function | delay */
transition: margin-right 4s ease-in-out 1s;

The problem starts when developers write transition: all .3s ease instead of specifying the exact property they want to animate. During page load, an above-the-fold element like a navigation menu transitions from its initial (unstyled) state to its final (styled or even hidden) state. If the transition property is all, this includes width, height, and even visibility. The result: a layout shift that only happens during loading and is nearly impossible to reproduce consistently.

Which CSS properties cause layout shift?

Not all CSS transitions cause layout shifts. Only properties that change the geometry or position of an element trigger the browser's layout step. According to Google's research, pages that animate layout-inducing CSS properties are 15% less likely to pass CLS. Pages that animate margin or border-width have poor CLS at nearly twice the normal rate.

Properties that cause layout shift: width, height, margin, padding, top, left, right, bottom, border-width, font-size, display

Safe properties (composite-only, no layout shift): transform, opacity, filter, clip-path

Safe properties (paint-only, no layout shift): background-color, color, box-shadow, outline

The key insight: transform and opacity are handled entirely by the browser's compositor thread. They never trigger layout recalculation, so they never count toward CLS. If you need to move an element, use transform: translate() instead of top/left. If you need to resize visually, use transform: scale() instead of width/height.

Take a look at the example below. This demonstrates a layout shift caused by CSS transitions that occur during the loading phase of a page. I see this pattern all the time and finding and fixing these kinds of issues can be difficult.

Find and fix CSS transitions

To find and fix all layout shifts caused by CSS transitions we need to run a quick test. First we find all CSS transitions on the page. Then we check whether any of them change layout-affecting properties. Finally we measure the impact of disabling or modifying those transitions to confirm they were causing CLS.

Core Web Vitals tip: Cumulative Layout Shifts that are caused by CSS transitions often occur early during the loading phase of the page. These layout shifts do not happen consistently which makes them hard to debug. Slowing down your network by emulating a mobile device and disabling your cache will make finding them easier!

Step 1: Find CSS transitions

Finding CSS transitions can be done manually: inspect all the stylesheets and search for the word 'transition'. That should not be more than 10 minutes work but there is a better way! Just paste this snippet in the console and press enter

(() => {

  let nodeTable = [];
  let nodeArray = [];

  // Get the name of the node
  function getName(node) {
    const name = node.nodeName;
    return node.nodeType === 1
      ? name.toLowerCase()
      : name.toUpperCase().replace(/^#/, '');
  }

  // Get the selector
  const getSelector = (node) => {
    let sel = '';

    try {
      while (node && node.nodeType !== 9) {
        const el = node;
        const part = el.id
          ? '#' + el.id
          : getName(el) +
          (el.classList &&
            el.classList.value &&
            el.classList.value.trim() &&
            el.classList.value.trim().length
            ? '.' + el.classList.value.trim().replace(/\s+/g, '.')
            : '');
        if (sel.length + part.length > (100) - 1) return sel || part;
        sel = sel ? part + '>' + sel : part;
        if (el.id) break;
        node = el.parentNode;
      }
    } catch (err) {
      // Do nothing...
    }
    return sel;
  };

  const getNodesWithTransition = (node) => {

    // Get the computed style
    let cs = window.getComputedStyle(node);
    let tp = cs['transition-property'];
    let td = cs['transition-duration'];

    // If there is a transition, add it to the table
    if (tp !== '' && tp !== 'none' && td != '0s') {
      nodeTable.push({ selector: getSelector(node), transition: cs['transition'] });
      nodeArray.push(node);
    }

    // Recursively call this function for each child node
    for (let i = 0; i < node.children.length; i++) {
      getNodesWithTransition(node.children[i]);
    }
  }

  // find all transitions
  getNodesWithTransition(document.body);

  // Display the results in the console
  console.log('%cReadable table of selectors and their transitions', 'color: red; font-weight: bold;');
  console.table(nodeTable);

  console.log('%cNodeList for you to inspect (harder to read but more info)', 'color: red; font-weight: bold;');
  console.log(nodeArray);


  // styles to temporarity override the transitions
  let selectors = nodeTable.map((item) => item.selector).join(', ');

  console.log('%cSpecific CSS to disable all transitions on this page', 'color: red; font-weight: bold;');
  console.log(`<style>${selectors}{transition-property: none !important;}</style>`);

  console.log('%cGlobal CSS to disable all transitions on this page (not suggested on production)', 'color: red; font-weight: bold;');
  console.log(`<style>*{transition-property: none !important;}</style>`);

})()

It will show you a table of all the transitions, the elements they are working on and more detail about the transitions.

To find layout shifts, look for transition properties like width, height, margin, padding, top, left, display and especially all (since all includes every valid transition property).

Step 2: Modify CSS transitions

The above JavaScript snippet will show all the transitions as well as provide example code on how to disable those transitions. For quick testing purposes I suggest you take the easy road and disable all transitions with one simple line of CSS code

<style>*{transition-property: none !important;}</style>

Of course for live environments a bit more finesse is required. Carefully only remove unneeded transition-properties on a per-selector basis. For example change #button{transition: all .2s} to #button{transition: background-color .2s}

Step 3: Measure the change in layout shift

The next and final step is to measure the impact. You can use my chrome extension Core Web Vitals Visualizer or a RUM tool like CoreDash to measure the real-life impact of these code changes. In CoreDash monitoring data, sites that replaced transition: all with specific properties saw their CLS improve by [CD:placeholder]% on average.

Transition best practices

  1. Always specify the exact property: Never use transition: all. Write transition: background-color .2s ease instead. This prevents accidental layout shifts from properties you did not intend to animate.
  2. Use transform and opacity for animations: These two properties are handled by the compositor and never trigger layout. Use transform: translate() to move elements, transform: scale() to resize them visually, and opacity to fade them. This is what Google recommends for high-performance animations.
  3. Set explicit dimensions on animated elements: If an element needs to transition, make sure it has explicit width and height (or an aspect ratio) before and after the transition. This prevents surrounding content from shifting. For more on this, see fixing layout shifts caused by auto-sizing images.
  4. Be careful with will-change: The will-change CSS property tells the browser to prepare for an animation by promoting the element to its own compositor layer. But it comes with trade-offs: it creates a new stacking context, increases GPU memory usage, and establishes a containing block for positioned descendants. Apply it dynamically with JavaScript just before the animation starts, and remove it when the animation ends. Do not leave will-change in your stylesheet permanently.

Despite all this, the 2025 Web Almanac reports that 40% of mobile pages still run non-composited animations. The good news: CLS is the Core Web Vital with the highest pass rate at 81% on mobile. The bad news: if your site is in the failing 19%, CSS transitions might be the cause you have not checked yet.

About the author

Arjen Karel is a web performance consultant and the creator of CoreDash, a Real User Monitoring platform that tracks Core Web Vitals data across hundreds of sites. He also built the Core Web Vitals Visualizer Chrome extension. He has helped clients achieve passing Core Web Vitals scores on over 925,000 mobile URLs.

17 years of fixing PageSpeed.

I have optimized platforms for some of the largest publishers and e-commerce sites in Europe. I provide the strategy, the code, and the RUM verification. Usually in 1 to 2 sprints.

View Services
Layout Shift caused by CSS transitionsCore Web Vitals Layout Shift caused by CSS transitions