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

Layout Shift Caused by CSS Transitions: Understanding and Mitigating the Impact

As web developers strive to create seamless and engaging user experiences, the use of CSS transitions has become increasingly prevalent. These transitions allow for smooth animations and dynamic changes to elements on a webpage, enhancing the overall user interface. However, one common and often overlooked issue associated with CSS transitions is the potential for layout shifts, which can negatively impact user experience and page performance.

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

Understanding CSS Transitions:

CSS transitions are a powerful tool for animating the change of a property over time. They are commonly used for effects such as fading, sliding, and scaling elements on a webpage. Developers can define transition effects by specifying the property to be transitioned, the duration of the transition, and the timing function governing the transition's acceleration.

Transsition can have a property, duration, timing-function and a delay. A transition shorthand looks like this:

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

Layout Shifts: The Unintended Consequence:

Layout shifts occur when elements on a webpage change position or size, causing other elements to reflow and the overall layout of the page to shift. While CSS transitions are designed to provide smooth animations, they can inadvertently trigger layout shifts, leading to a jarring and disruptive user experience. The most common causes of layout shifts during CSS transitions include changes in dimensions, position, or visibility of elements.

Cumulatieve Layout Shifts caused by CSS transitions usually occur when a above-the-fold element like a navigation menu transitions from their first (unstyled) state to their final (styled or even hidden) state. This is usually an unintended consequence of overly broad transition property's. For example a menu entry should only transition background color and instead of the transition property 'background-color' 'all' has been chosen. This will lead to not only a background transition but in some cases also a width, height or even visibility transition during page load.

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. Unfortunately 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 do a quick test. First we need to find all CSS transitions. When we have done this we need to ensure the transition does not change the position (width, height,margin,padding, visibility) of an element. We can do this by modifying or disabling these transitions. Then finally we can test the impact of these changes and decide one and for all if CSS transitions are causing CLS issues. 

Core Web Vitals tip: Cumulative Layout Shifts that are caused by CSS transition 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 then 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 off all the transitions, the elements they are working on and more detail about the transitions.

cls snippet table

To find layout shift we need to be look for transition property's like width,height, margin,paddingtransform, display and especially all (since all includes all valid transition properties)

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 that to 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-base. 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.

layout shift transition measured by coredash


Other transition good-practices:

  1. Prefer GPU Acceleration: Utilizing GPU acceleration for CSS transitions can offload the rendering workload from the CPU to the GPU. This can be achieved by ensuring that the properties being transitioned are conducive to GPU acceleration, such as opacity and transform.
  2. Use the "will-change" Property: The will-change CSS property informs the browser that a specific element is likely to be changed, allowing it to optimize rendering accordingly. 
  3. Ensure Consistent Dimensions: To prevent layout shifts caused by changes in dimensions, ensure that elements have consistent dimensions before and after the transition. This may involve setting explicit dimensions, using percentage-based values, or employing techniques like aspect ratio boxes.
  4. Optimize Timing Functions: The choice of timing function can significantly impact the perception of smoothness during a transition. Be mindful of the acceleration and deceleration patterns, and consider using ease-in-out or custom cubic bezier functions for a more natural feel.

I help teams pass the Core Web Vitals:

lighthouse 100 score

A slow website is likely to miss out on conversions and revenue. Nearly half of internet searchers don't wait three seconds for a page to load before going to another site. Ask yourself: "Is my site fast enough to convert visitors into customers?"

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