Learn Measure Blog About

Cumulative Layout Shift (CLS)

Updated
Appears in: Metrics

Cumulative Layout Shift (CLS) is an important, user-centric metric for measuring visual stability because it helps quantify how often users experience unexpected layout shifts—a low CLS helps ensure that the page is delightful.

Have you ever been reading an article online when something suddenly changes on the page? Without warning, the text moves, and you've lost your place. Or even worse: you're about to tap a link or a button, but in the instant before your finger lands—BOOM—the link moves, and you end up clicking something else!

Most of the time these kinds of experiences are just annoying, but in some cases, they can cause real damage.

A screencast illustrating how layout instability can negatively affect users.

Unexpected movement of page content usually happens because resources are loaded asynchronously or DOM elements get dynamically added to the page above existing content. The culprit might be an image or video with unknown dimensions, a font that renders larger or smaller than its fallback, or a third-party ad or widget that dynamically resizes itself.

What makes this issue even more problematic is that how a site functions in development is often quite different from how users experience it. Personalized or third-party content often doesn't behave the same in development as it does in production, test images are often already in the developer's browser cache, and API calls that run locally are often so fast that the delay isn't noticeable.

The Cumulative Layout Shift (CLS) metric helps you address this problem by measuring how often it's occurring for real users.

What is CLS?

CLS measures the sum total of all individual layout shift scores for every unexpected layout shift that occurs during the entire lifespan of the page.

A layout shift occurs any time a visible element changes its position from one frame to the next. (See below for details on how individual layout shift scores are calculated.)

Good CLS values are under 0.1, poor values are greater than 0.25
            and anything in between needs improvement

What is a good CLS score?

To provide a good user experience, sites should strive to have a CLS score of less than 0.1. To ensure you're hitting this target for most of your users, a good threshold to measure is the 75th percentile of page loads, segmented across mobile and desktop devices.

To learn more about the research and methodology behind this recommendation, see: Defining the Core Web Vitals metrics thresholds

Layout shifts in detail

Layout shifts are defined by the Layout Instability API, which reports layout-shift entries any time an element that is visible with the viewport changes its start position (for example, its top and left position in the default writing mode) between two frames. Such elements are considered unstable elements.

Note that layout shifts only occur when existing elements change their start position. If a new element is added to the DOM or an existing element changes size, it doesn't count as a layout shift—as long as the change doesn't cause other visible elements to change their start position.

Layout shift score

To calculate the layout shift score, the browser looks at the viewport size and the movement of unstable elements in the viewport between two rendered frames. The layout shift score is a product of two measures of that movement: the impact fraction and the distance fraction (both defined below).

layout shift score = impact fraction * distance fraction

Impact fraction

The impact fraction measures how unstable elements impact the viewport area between two frames.

The union of the visible areas of all unstable elements for the previous frame and the current frame—as a fraction of the total area of the viewport—is the impact fraction for the current frame.

Impact fraction example with one unstableelement

In the image above there's an element that takes up half of the viewport in one frame. Then, in the next frame, the element shifts down by 25% of the viewport height. The red, dotted rectangle indicates the union of the element's visible area in both frames, which, in this case, is 75% of the total viewport, so its impact fraction is 0.75.

Distance fraction

The other part of the layout shift score equation measures the distance that unstable elements have moved, relative to the viewport. The distance fraction is the greatest distance any unstable element has moved in the frame (either horizontally or vertically) divided by the viewport's largest dimension (width or height, whichever is greater).

Distance fraction example with one unstableelement

In the example above, the largest viewport dimension is the height, and the unstable element has moved by 25% of the viewport height, which makes the distance fraction 0.25.

So, in this example the impact fraction is 0.75 and the distance fraction is 0.25, so the layout shift score is 0.75 * 0.25 = 0.1875.

Initially, the layout shift score was calculated based only on impact fraction. The distance fraction was introduced to avoid overly penalizing cases where large elements shift by a small amount.

The next example illustrates how adding content to an existing element affects the layout shift score:

Layout shift example with stable and unstable elements and viewportclipping

The "Click Me!" button is appended to the bottom of the gray box with black text, which pushes the green box with white text down (and partially out of the viewport).

In this example, the gray box changes size, but its start position does not change so it's not an unstable element.

The "Click Me!" button was not previously in the DOM, so its start position doesn't change either.

The start position of the green box, however, does change, but since it's been moved partially out of the viewport, the invisible area is not considered when calculating the impact fraction. The union of the visible areas for the green box in both frames (illustrated by the red, dotted rectangle) is the same as the area of the green box in the first frame—50% of the viewport. The impact fraction is 0.5.

The distance fraction is illustrated with the purple arrow. The green box has moved down by about 14% of the viewport so the distance fraction is 0.14.

The layout shift score is 0.5 x 0.14 = 0.07.

This last example illustrates multiple unstable elements:

Layout shift example with multiple stable and unstableelements

In the first frame above there are four results of an API request for animals, sorted in alphabetical order. In the second frame, more results are added to the sorted list.

The first item in the list ("Cat") does not change its start position between frames, so it's stable. Similarly, the new items added to the list were not previously in the DOM, so their start positions don't change either. But the items labelled "Dog", "Horse", and "Zebra" all shift their start positions, making them unstable elements.

Again, the red, dotted rectangles represent the union of these three unstable elements' before and after areas, which in this case is around 38% of the viewport's area (impact fraction of 0.38).

The arrows represent the distances that unstable elements have moved from their starting positions. The "Zebra" element, represented by the blue arrow, has moved the most, by about 30% of the viewport height. That makes the distance fraction in this example 0.3.

The layout shift score is 0.38 x 0.3 = 0.1172.

Expected vs. unexpected layout shifts

Not all layout shifts are bad. In fact, many dynamic web applications frequently change the start position of elements on the page.

User-initiated layout shifts

A layout shift is only bad if the user isn't expecting it. On the other hand, layout shifts that occur in response to user interactions (clicking a link, pressing a button, typing in a search box and similar) are generally fine, as long as the shift occurs close enough to the interaction that the relationship is clear to the user.

For example, if a user interaction triggers a network request that may take a while to complete, it's best to create some space right away and show a loading indicator to avoid an unpleasant layout shift when the request completes. If the user doesn't realize something is loading, or doesn't have a sense of when the resource will be ready, they may try to click something else while waiting—something that could move out from under them.

Layout shifts that occur within 500 milliseconds of user input will have the hadRecentInput flag set, so they can be excluded from calculations.

Animations and transitions

Animations and transitions, when done well, are a great way to update content on the page without surprising the user. Content that shifts abruptly and unexpectedly on the page almost always creates a bad user experience. But content that moves gradually and naturally from one position to the next can often help the user better understand what's going on, and guide them between state changes.

CSS transform property allows you to animate elements without triggering layout shifts:

  • Instead of changing the height and width properties, use transform: scale().
  • To move elements around, avoid changing the top, right, bottom, or left properties and use transform: translate() instead.

How to measure CLS

CLS can be measured in the lab or in the field, and it's available in the following tools:

Field tools

Lab tools

Measure CLS in JavaScript

The easiest way to measure CLS (as well as all Web Vitals field metrics) is with the web-vitals JavaScript library, which wraps all the complexity of manually measuring CLS into a single function:

import {getCLS} from 'web-vitals';

// Measure and log the current CLS value,
// any time it's ready to be reported.
getCLS(console.log);

To manually measure CLS, you can use the Layout Instability API. The following example shows how to create a PerformanceObserver that listens for individual layout-shift entries and logs them to the console:

// Use a try/catch instead of feature detecting `layout-shift`
// support, since some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry);
}
});

po.observe({type: 'layout-shift', buffered: true});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}

CLS is the sum of those individual layout-shift entries that didn't occur with recent user input. To calculate CLS, declare a variable that stores the current score, and then increment it any time a new, unexpected layout shift is detected.

Rather than reporting every change to CLS (which could happen very frequently), it's better to keep track of the current CLS value and report it any time the page's lifecycle state changes to hidden:

// Sends the passed data to an analytics endpoint. This code
// uses `/analytics`; you can replace it with your own URL.
function sendToAnalytics(data) {
const body = JSON.stringify(data);
// Use `navigator.sendBeacon()` if available, falling back to `fetch()`.
(navigator.sendBeacon && navigator.sendBeacon('/analytics', body)) ||
fetch('/analytics', {body, method: 'POST', keepalive: true});
}

// Use a try/catch instead of feature detecting `layout-shift`
// support, since some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// Store the current layout shift score for the page.
let cls = 0;

function onLayoutShiftEntry(entry) {
// Only count layout shifts without recent user input.
if (!entry.hadRecentInput) {
cls += entry.value;
}
}

// Create a PerformanceObserver that calls `onLayoutShiftEntry` for each entry.
const po = new PerformanceObserver((entryList) => {
entryList.getEntries().forEach(onLayoutShiftEntry);
});

// Observe entries of type `layout-shift`, including buffered entries,
// i.e. entries that occurred before calling `observe()` below.
po.observe({
type: 'layout-shift',
buffered: true,
});

// Log the current CLS score any time the
// page's lifecycle state changes to hidden.
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
// Force any pending records to be dispatched.
po.takeRecords().forEach(onLayoutShiftEntry);

// Report the CLS value to an analytics endpoint.
sendToAnalytics({cls});
}
});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}

Note: CrUX buckets CLS values as percentages with 5% granularity. This means a score of 0.01 when using the code example above would appear in the 0–5 bucket in CrUX, and a score of 0.07 would appear in the 5–10 bucket in CrUX.

How to improve CLS

For most websites, you can avoid all unexpected layout shifts by sticking to a few guiding principles:

  • Always include size attributes on your images and video elements, or otherwise reserve the required space with something like CSS aspect ratio boxes. This approach ensures that the browser can allocate the correct amount of space in the document while the image is loading. Note that you can also use the unsized-media feature policy to force this behavior in browsers that support feature policies.
  • Never insert content above existing content, except in response to a user interaction. This ensures any layout shifts that occur are expected.
  • Prefer transform animations to animations of properties that trigger layout changes. Animate transitions in a way that provides context and continuity from state to state.

For a deep dive on how to improve CLS, see Optimize CLS.

Additional resources

CHANGELOG

Occasionally, bugs are discovered in the APIs used to measure metrics, and sometimes in the definitions of the metrics themselves. As a result, changes must sometimes be made, and these changes can show up as improvements or regressions in your internal reports and dashboards.

To help you manage this, all changes to either the implementation or definition of these metrics will be surfaced in this CHANGELOG.

Last updated: Improve article