lucky.graphics
guides
abouttools

guides

Cumulative Layout Shift Score Worked Examples for Designers

A layout shift score is impact fraction times distance fraction. Worked scores for an unsized image, cookie banner, late ad, accordion and header on 390x844 and 1440x900.

Published
November 3, 2026
Topic
guides
Reading time
35 min
Review
Reviewed against the primary source

Cumulative Layout Shift Score Worked Examples for Designers

Cumulative Layout Shift multiplies the share of the viewport that moving content covers by how far it travelled, then keeps the worst burst. A 168 px cookie banner pushing a phone page down scores 0.185, against a good limit of 0.1. Overlaid at the bottom, it scores 0.

web.dev's own worked example is 0.75 × 0.25 = 0.1875, and the arithmetic really is that short. The hard part is knowing which elements the browser counts as having moved, what area they cover before and after, and which viewport the two fractions are measured against. That last choice moves the answer a long way. A 200 px push scores 0.140 in a 390x844 viewport and 0.046 in a 1440x900 one, a ratio of 3.03, for two reasons: the phone's longer side is 844 px against the desktop's 1440 px, and an article column fills 92 percent of the phone's width but half of the desktop's. A pattern that passes comfortably in a desktop mockup can fail on the phone it was never reviewed on.

Score of each unfixed pattern on a phone and a desktop viewportHorizontal bar chart of layout shift scores. Image without dimensions: 0.173 on 390x844 and 0.108 on 1440x900. Banner that pushes the page: 0.185 and 0.035. Ad at y 300: 0.175 and 0.058. FAQ opened after paint: 0.094 and 0.021. Header that grows at load: 0.024 and 0.010. Dashed lines mark the 0.1 good limit and the 0.25 poor limit.390x8441440x900Image, no dimensionsImage, no dimensions, 390x844: 0.1730.173Image, no dimensions, 1440x900: 0.1080.108Banner pushes the pageBanner pushes the page, 390x844: 0.1850.185Banner pushes the page, 1440x900: 0.0350.035Ad, 300x250 at y 300Ad, 300x250 at y 300, 390x844: 0.1750.175Ad, 300x250 at y 300, 1440x900: 0.0580.058FAQ opens after paintFAQ opens after paint, 390x844: 0.0940.094FAQ opens after paint, 1440x900: 0.0210.021Header grows at loadHeader grows at load, 390x844: 0.0240.024Header grows at load, 1440x900: 0.0100.0100.10.25
Score of each unfixed pattern on a phone and a desktop viewportEach bar is one shift in the page model on this page, before the fix. Dashed lines: 0.1 (good) and 0.25 (poor). The phone bar is first in every group.Source: Layout Instability draft (WICG, 17 December 2025) and web.dev CLS guide, checked November 3, 2026; scores are our arithmetic on the stated page model.
Show the data as a table
Pattern390x8441440x900
Image, no dimensions0.1730.108
Banner pushes the page0.1850.035
Ad, 300x250 at y 3000.1750.058
FAQ opens after paint0.0940.021
Header grows at load0.0240.010

What the browser counts, in the order it counts it

The Layout Instability specification defines the score for a single rendered frame. Cumulative Layout Shift is built on top of those per-frame values, so it helps to take the pieces in order.

Which nodes count as moved. A node is unstable when it is an element that generates boxes, or a text node, is visible, and its starting point (the top-left corner in a left-to-right page) differs from the previous frame by a significant amount in both the viewport's coordinate space and the document's. The specification leaves the amount to each browser and notes: "Chrome has defined number of pixels to significance as 3." So a movement under 3 px does not make a node unstable in Chrome. Only existing nodes that change their starting point count. web.dev puts it this way: an element that is added to the page, or one that changes size, is not a layout shift in itself, and the shift is recorded only if the change makes other visible elements start somewhere else. That is why the image in the first pattern never appears in its own score: its top edge stays where it was, and the text underneath it is what moves.

The impact fraction. web.dev defines it this way: "The impact fraction measures how unstable elements impact the viewport area between two frames." Every unstable node contributes both the area it occupied in the previous frame and the area it occupies now, each clipped to the viewport, and the browser takes the union of all of those rectangles. Overlap is counted once, so a paragraph inside a section inside a column does not count three times. Anything that moved off-screen contributes nothing. The union, divided by the viewport's area, is the impact fraction.

The distance fraction. web.dev: "The distance fraction is the greatest horizontal or vertical distance any unstable element has moved in the frame divided by the viewport's largest dimension (width or height, whichever is greater)." The specification adds that the fraction is capped at 1. Only the single largest mover matters. Fifty paragraphs that all slide down 200 px produce the same distance fraction as one paragraph that slides 200 px.

The score. Impact fraction times distance fraction, per frame. The script that backs this page reproduces web.dev's three worked examples (0.75 × 0.25 = 0.1875, 0.5 × 0.14 = 0.07 and 0.6 × 0.3 = 0.18) and also rebuilds the first one from rectangles. Take a 400 by 800 viewport and a full-width element 400 px tall that moves down 200 px. The union of its two positions is 400 by 600, which is 0.75 of the viewport. The 200 px move is 0.25 of the 800 px long side. The product is 0.1875.

What is excluded. Shifts within 500 ms of a discrete user input are flagged. web.dev: "Layout shifts that occur within 500 milliseconds of user input will have the hadRecentInput flag set, so they can be excluded from calculations." The specification lists mousedown, keydown, pointerdown and change as excluding inputs and adds: "However, an event whose only effect is to begin or update a flick or scroll gesture is not an excluding input." A tap on a button buys half a second of grace. Scrolling buys nothing. A change to a transform does not make a node unstable by itself, because the specification compares starting points both with and without transforms, which is why a transform-based slide-in is free where a margin-based one is not.

Session windows. web.dev: "A burst of layout shifts, known as a session window, is when one or more individual layout shifts occur in rapid succession with less than 1-second in between each shift and a maximum of 5 seconds for the total window duration." The page's CLS is the largest window total. The web-vitals library implements this as a comparison of each new shift's start time with the previous shift (under 1000 ms) and with the first shift in the window (under 5000 ms), and it skips entries that had recent input.

Thresholds. Good is 0.1 or less and poor is above 0.25, judged at the 75th percentile of page loads, segmented across mobile and desktop. The 0.1 line has a documented origin. The Chrome team's threshold write-up says: "In our internal testing, we found that levels of shift from 0.15 and higher were consistently perceived as disruptive, while shifts of 0.1 and lower were noticeable but not excessively disruptive." The 0.25 line was set where roughly 20 percent of phone origins and 18 percent of desktop origins in CrUX data from April 2020 fell above it.

Two cautions apply before any hand calculation. The specification says the value is not meant to be high-precision and that a browser may trade precision for efficiency, so treat a hand result as good to a few percent. And the calculation is per frame: when a shift happens over several frames, each frame is scored on its own and the scores add up, which matters in the animation cases below.

One idealised page, two viewports

Every table below uses the same page skeleton, with the heights and widths in this table. These are chosen inputs for the calculation, not measurements of a real site. Replace them with your own layout's numbers and the method still holds.

Input390x844 (phone)1440x900 (desktop)
Viewport area329,160 px²1,296,000 px²
Longer side (distance denominator)844 px (height)1440 px (width)
Article column16 px gutters, 358 px wide720 px wide, centred
Column as a share of viewport width0.9180.500
Header height56 px, full width72 px, full width
Title block120 px140 px
Hero image, 16:9 at column width201.375 px tall405 px tall
Hero image top edge176 px212 px
Cookie banner height168 px88 px
Ad slot top edge (scored as a column of text below this y)300 px300 px
FAQ answer panel height150 px100 px
First FAQ question row, bottom edge (the panel opens below it)356 px356 px

The body text under the image is treated as one solid column that runs past the bottom of the viewport. Real paragraphs have margins between them, which leaves gaps in the union. With 144 px paragraphs and 16 px gaps, the unfixed image case scores 0.169 on the phone instead of 0.173, and 0.103 on the desktop instead of 0.108. The solid model overstates by two to five percent, which is inside the tolerance the specification itself asks for. The ad slot and accordion rows are each scored as that solid column below the stated y, on its own, and not stacked under the hero above. The hero spans 176 to 377 px on the phone and 212 to 617 px on the desktop, so the y = 300 slot and the 356 px accordion edge sit inside its band on both, and the y = 600 slot does on the desktop. Read those rows as separate page layouts without that hero.

A word on the viewport names. "1440x900" is a CSS viewport, the layout width and height in CSS pixels that a browser window offers the page, and not a monitor size. "390x844" is a CSS viewport of the kind current phones present at default zoom. The calculation uses the larger of width and height as the distance denominator, so the desktop window divides by its width and the phone by its height.

Three differences between the viewports drive almost every result below.

  • The denominator. The same pixel move is a smaller fraction of 1440 than of 844. The ratio is 1440 ÷ 844 = 1.71.
  • The column share. On the phone the text column covers 92 percent of the width, so a vertical push impacts nearly the full width of everything below it. On the desktop the column covers half, so the impact fraction cannot exceed about 0.5 for column content.
  • The height of the thing that moves. A 16:9 image is 201 px tall on the phone and 405 px on the desktop. The desktop push is larger in pixels and still smaller in score.
Same 200 px push of the column below y = 300ImpactDistanceScore
390x8440.5920.2370.140
1440x9000.3330.1390.046

Pattern 1: an image with no dimensions

Start with the most common cause web.dev lists, an image whose size the browser learns only when the file arrives. In the early days of the web, developers put width and height on the img tag. Responsive design led many to drop them in favour of CSS such as width 100 percent and height auto, and web.dev explains the consequence: "However, because the image size isn't specified, space can't be allocated for it until the browser starts to download it and can determine its dimensions." Until then the image occupies no height, so the text below starts right under the title.

When the file lands, the image box grows from nothing to 201.4 px on the phone and the whole body column drops by that amount. The image's own top edge did not move, so it is not an unstable node. The body column is. Its union of before and after positions spans from y = 176 to the bottom of the viewport.

CaseViewportLargest moveImpactDistanceScore
No width or height, 16:9 file arrives390x844201.4 px0.7270.2390.173
No width or height, 16:9 file arrives1440x900405 px0.3820.2810.108
width and height set to the file's ratioboth0 px000
Attributes say 4:3, file is 16:9390x84467.1 px up0.5080.0800.040
Attributes say 4:3, file is 16:91440x900135 px up0.1570.0940.015
Image starts at y = 1400, below the foldbothnot in view00.239 / 0.2810

One image costs 1.73 times the whole phone budget and lands at 0.108 against the desktop's 0.1 line. The desktop result is that high only because the desktop image is large. A 16:9 hero at 720 px wide is a 405 px push in a 900 px window, which is nearly half the viewport height.

The fix. Put the file's intrinsic dimensions on the element and let CSS scale it:

<img src="hero-1600.avif" width="1600" height="900" alt="Studio desk with two monitors">
img { max-width: 100%; height: auto; }

web.dev explains why this works: major browsers compute a default aspect ratio from the width and height attributes as the HTML is parsed, before any file is fetched, and the ratio is displayed as "aspect-ratio: auto 640 / 360" in Chrome's styles panel. As soon as CSS gives the image a width, the ratio supplies the height. The attributes do not need to match the displayed size, only the ratio. MDN describes the same mechanism for the aspect-ratio property: "If both are given, and the element is a replaced element, such as <img>, then the given ratio is used until the content is loaded." For elements that are not images, such as a video poster wrapper or an embed container, write the ratio yourself with aspect-ratio: 16 / 9.

The wrong-ratio row matters more than it looks. A designer who exports a 16:9 hero but leaves a template's 4:3 attributes in place reserves 268.5 px on the phone and gets 201.4 px, a 67.1 px shift upward. That scores 0.040, which is not alarming alone, but web.dev is explicit that the browser's "auto" ratio takes over once the file loads, so a wrong guess still moves the text. The reserved slot should match the file.

Art direction needs a ratio per source. When a picture element swaps a cropped image on narrow screens, the ratios differ between sources. web.dev shows width and height set on each source element and on the fallback img, and says the three major browsers support this. With srcset alone, web.dev advises that every candidate share one aspect ratio so that a single pair of attributes is true for all of them. Our responsive images guide covers how to write the candidate list and the sizes attribute; the shift arithmetic here is the reason the ratio rule in that guide exists.

The below-the-fold row is a trap if you stop reading there. The 0 in the last row is real for the moment of load. The browser ignores the invisible area, and web.dev's example of a box pushed partly out of the viewport shows the same effect. But web.dev also warns that "CLS often happens while the user scrolls through a page, when lazy-loaded content loads fully without space reserved for it." Once a reader scrolls the same image into the viewport and it resolves late, the 0.173 comes back. Lazy loading changes when the shift happens, not whether the image needs dimensions.

The file size and format of the hero image belong to a different problem. How many kilobytes it can weigh before the Largest Contentful Paint slips is worked out in our hero image file size and LCP budget guide, and the image formats guide covers which format to ship. Neither changes the shift. A 40 KB AVIF without dimensions pushes the text by the same distance as a 400 KB JPEG shown at the same size.

Pattern 2: a cookie banner that pushes the page, and one that floats

web.dev's cookie notice guidance describes the pattern: "Top-of-screen cookie notices are a very common source of layout shift. If a cookie notice is inserted into the DOM after the surrounding page has already rendered, it will push the page elements below it further down the page." Everything below the banner moves, including the header, so the impact region is nearly the whole viewport.

On the phone the banner is 168 px tall: three lines of consent text and two stacked buttons. On the desktop it is one 88 px strip. Both are illustrative heights.

CaseViewportLargest moveImpactDistanceScore
Banner inserted above the header, pushes page down390x844168 px0.9290.1990.185
Banner inserted above the header, pushes page down1440x90088 px0.5800.0610.035
Same banner as a fixed overlay at the bottomboth0 px000
Space reserved in the HTML, banner fills itboth0 px000
Banner grows by animating height over 300 ms390x844168 px over 18 framessummedsummed0.163
Banner grows by animating height over 300 ms1440x90088 px over 18 framessummedsummed0.029

The phone number is 1.85 times the budget and the desktop number is 0.35 times it. That gap is why a banner passes a desktop review and fails in the field. The phone impact fraction is 0.929 because the full-width header lands in a new place and the column below it does too. The distance fraction is 0.199 because the banner is about a fifth of the phone's height.

Animating height does not rescue it. A banner that slides in by growing its height is scored frame by frame, and the frame scores add up. With a CSS ease curve over 18 frames, the sum is 0.163 on the phone, not zero. Four of the 18 frames move less than 3 px, and Chrome's threshold drops those from the sum, which is why the figure is a little under the 0.185 of an instant insertion. That is a quirk of the threshold and not a technique. The reliable options are the two in the table that score zero.

The fix, version one: float it. web.dev's cookie guidance names this when space cannot be reserved, for example when the notice's dimensions vary by geography, and recommends a sticky footer or a modal: "Because both of these alternative approaches display the cookie notice as an "overlay" on top of the rest of the page, the cookie notice shouldn't cause content shifts when it loads." A bottom sheet:

.consent {
  position: fixed;
  inset: auto 0 0 0;
  max-height: 40vh;
  overflow: auto;
}

An overlay does not change any other node's starting point, so the score is 0. Its cost is that it covers content: 168 px is 19.9 percent of the phone viewport. If the buttons inside are small, they also have to meet the target-size rules in our touch target size guide, because a consent button that is hard to hit is a different failure.

The fix, version two: reserve it. If the banner height is predictable, put an empty container in the server-rendered HTML with the right height at each breakpoint, and let the script fill it:

.consent-slot { min-height: 168px; }
@media (min-width: 960px) { .consent-slot { min-height: 88px; } }

This avoids covering content but leaves a blank strip for visitors who have already answered, unless the server knows not to render the slot for them. It also has a second shift to plan for. When the banner leaves, the reserved space collapses and the page jumps back up by the same distance. web.dev flags the general version: "Removing the space set aside for elements can cause just as much CLS as inserting content." Removal would score 0.185 again on the phone. Whether it counts depends on timing. A click on Accept is an excluding input, so a removal in the next 500 ms is flagged and ignored. A removal that waits for a consent-platform round trip and lands later than 500 ms after the click is scored, because the input exclusion has expired. If the script that handles Accept does heavy work first, the banner can disappear after the grace period. web.dev's cookie guidance says the Chrome team worked with a number of consent platforms to yield after Accept, so the browser can acknowledge the click in the next paint. Hide the banner on the click, and do the slow work afterwards.

Pattern 3: an ad slot that fills late

An ad is an image whose size depends on an auction. web.dev says: "Ads are one of the largest contributors to layout shifts on the web." Ad networks often support dynamic ad sizes, which is the reason. Google's Publisher Tag guide describes the mechanism: the slot is requested asynchronously, and "If you don't reserve sufficient space for the ads being loaded, they can end up displacing visible non-ad content when they are eventually added to the page."

The scored case here is a 300x250 creative that appears at y = 300 in the middle of the article text on both viewports. The text beneath the slot is the unstable node.

CaseViewportLargest moveImpactDistanceScore
No reservation, 300x250 arrives at y = 300390x844250 px0.5920.2960.175
No reservation, 300x250 arrives at y = 3001440x900250 px0.3330.1740.058
Same slot at y = 600390x844250 px0.2650.2960.079
Same slot at y = 6001440x900250 px0.1670.1740.029
Same slot at y = 1000, below the foldbothnot in view00.296 / 0.1740
250 px reserved with min-heightboth0 px000
100 px reserved, 250 px served390x844150 px0.4830.1780.086
100 px reserved, 250 px served1440x900150 px0.2780.1040.029
250 px reserved, slot shrinks to a 50 px creative390x844200 px up0.5370.2370.127
250 px reserved, slot shrinks to a 50 px creative1440x900200 px up0.3060.1390.042
250 px reserved, no ad fills, slot collapses390x844250 px up0.5920.2960.175
250 px reserved, no ad fills, slot collapses1440x900250 px up0.3330.1740.058

Position matters because the impact region is the part of the page below the slot that is inside the viewport. Moving the slot from y = 300 to y = 600 cuts the phone score from 0.175 to 0.079. web.dev says it directly: "Dynamically injected content closer to the top of the viewport usually causes greater layout shifts than content injected lower in the viewport." Google's guide agrees that the higher a slot sits in the viewport, the more content it can displace.

And the collapse row equals the insertion row. In this model, a slot that disappears when no ad fills scores what a late ad scores, because the same 250 px of content moves the same distance in the other direction. The shrink row is smaller because the move is 200 px, not 250.

The fix is a reservation, and the size depends on your creatives. Google recommends setting a size on the slot with CSS:

<div id="ad-slot" style="min-width: 300px; min-height: 250px;"></div>

Using min-height means the container can still grow if a larger creative arrives, so nothing is clipped. Add media queries where the slot's sizes differ by screen, for example a smaller minimum below 960 px. The guide advises against reserving the space with JavaScript, with this reason: "Reserving space with JavaScript should be avoided, since doing so can result in a layout shift at the time the script loads." Reserve in the stylesheet or inline style so that the space exists in the first layout.

For slots that accept several sizes, Google lists three approaches: reserve the largest, reserve the smallest, or reserve the size most likely to serve based on historical fill data. Reserving the largest removes the shift and leaves blank space when a smaller creative serves. The guide recommends keeping that space rather than shrinking the slot, since "Shrinking the ad slot to match the size of the served creative would incur an additional layout shift, so it's recommended to avoid doing this." Centre the creative inside the reservation instead. Fluid slots, which have no fixed size list, cannot be reserved at all, and the guide says they always shift, so use them below the fold and request them early.

How a reservation interacts with the 75th percentile. CLS is judged at the 75th percentile of page loads, so what matters is the score of the load at that rank, not the average. Google's guide gives two sample fill tables. Applying the reservation sizes to them, and assuming the ad is the only source of shift on the page:

Reserve (px)Creative mixViewportShare of loads that shiftPage CLS at p75
070% 300x250, 30% 320x50390x844100%0.175
5070% 300x250, 30% 320x50390x84470%0.127
10070% 300x250, 30% 320x50390x84470%0.086
25070% 300x250, 30% 320x50390x8440%0
070% 300x250, 30% 320x501440x900100%0.058
10070% 300x250, 30% 320x501440x90070%0.029
25070% 300x250, 30% 320x501440x9000%0
060% 970x90, 10% 728x90, 20% 320x50, 5% 728x250, 5% 300x2501440x900100%0.021
90same mix1440x90010%0

In the last two rows, reserving 90 px for the leaderboard-heavy mix leaves 10 percent of loads shifting, because only the two 250 px creatives overflow it. Those loads sit above the 75th percentile, so the page's CLS at that rank reads 0. The visitors who got the 250 px creative still saw their text jump. A clean p75 is permission to look at the tail, not a sign that the tail is fine. The leaderboard mix is shown for the desktop viewport only, since a 970x90 creative does not fit a 390 px screen or a 720 px column. In the 70/30 mix, by contrast, 70 percent of loads overflow a 100 px reservation, so the p75 is the shifted value of 0.086 on the phone: inside the budget, but using 86 percent of it on one slot.

Pattern 4: an accordion, and when a click stops being free

An accordion is the pattern where the layout shift is the point. The user taps a question, the answer opens, the questions beneath it move down. The metric is built to ignore that, and the rule has an edge.

On the phone, an answer panel is 150 px tall (six lines of 25 px), and on the desktop 100 px. The unstable nodes are everything under the opened item. The table gives the score the shift would have if nothing excluded it, then what the metric records.

CaseViewportShift if countedRecorded in CLS
Script opens the first item after paint, no input390x8440.0940.094
Script opens the first item after paint, no input1440x9000.0210.021
Tap opens the panel, 200 ms ease transition390x8440.0810
Tap opens the panel, 200 ms ease transition1440x9000.0190
Tap opens the panel, 800 ms linear transition390x8440.0800.028
Tap opens the panel, 800 ms linear transition1440x9000 (2.08 px per frame)0
Tap opens the panel, 800 ms ease transition390x8440.0640

The common case is free. A panel that opens on a tap and finishes inside 500 ms scores 0 in the metric, because every frame of the shift falls inside the window that sets hadRecentInput. That is why a plain click-to-open accordion, tab or "show more" button is not a CLS problem even though it moves a lot of content. A tap on a "Load more" button is the same: web.dev recommends having the user initiate the load so the shift is expected.

The surprise cases are the ones without an input. The first row is the version that does damage: a component that renders closed on the server and then opens the first item in an effect after it hydrates, or a script that restores the last-open panel from storage. No input happened, so the full 0.094 counts on the phone. The cure is boring and effective. Make the initial state part of the HTML, with the open attribute on a details element or the equivalent class on the server-rendered markup, so that the first layout is the final layout.

The 500 ms rule has an end. The 800 ms linear transition keeps moving after the grace period. Timed from the tap, 19 of its 48 frames on the phone come after 500 ms and carry 59.4 px of the 150 px, which scores 0.028 in the metric. The same panel on an ease curve front-loads its motion, and its late frames are below the 3 px threshold, so Chrome records nothing. That second result is a property of the threshold in the specification's note, not a design principle: another browser may define a different threshold, and the moment the panel is a little taller the late frames would be counted again. The usable rule is the opposite one. Keep panel transitions short, well under 500 ms, so that the whole motion is inside the grace period.

Two more ways to lose the exemption. web.dev names scrolling that triggers lazy-loaded content, and holding the pointer over content, as post-load causes of CLS, and says: "Any content shift during either of these interactions counts as unexpected, even if it happens within 500 milliseconds." A menu or preview that opens when the pointer rests on it is scored. And a panel whose content comes from the network can take longer than 500 ms to fill after the tap. web.dev's answer is to reserve the expected space inside the 500 ms window, so that the impact is taken up front and later shifts are not counted, for example with a skeleton at the expected height. A placeholder at the right height turns a counted shift into an excluded one.

For the choice between a layout animation and a transform on an accordion, our web animation performance guide covers frame cost. The scoring rule is simpler: a height change moves the content below it, and the only question is whether an input excuses it.

Pattern 5: a header that changes height

A header is the first thing in the document, so any change in its height moves everything under it. Two situations look similar and score differently.

The header grows at load. A late logo image without dimensions, a promo strip injected by a script, or navigation that wraps onto a second line after a font swap all make the header taller after first paint. At scroll position zero there is no scroll anchor to compensate, and the content under the header moves. Take a header that goes from 56 to 80 px on the phone (24 px) and from 72 to 104 px on the desktop (32 px). The body column is the unstable set.

CaseViewportLargest moveImpactDistanceScore
Header grows 24 px at load390x84424 px0.8570.0280.024
Header grows 32 px at load1440x90032 px0.4600.0220.010

These are small. The impact fraction is large, but the distance fraction is tiny, because 24 px is only 2.8 percent of 844. A header shift alone will not fail a page. It matters as a member of a burst, as the session-window section shows. The fix is to give the header a fixed height in CSS that includes the room for the tallest content it will hold, and to put width and height on the logo.

The header shrinks on scroll. The popular pattern is a sticky header that drops from 80 to 56 px once the reader scrolls past a threshold. Whether this scores depends on scroll anchoring. The CSS Scroll Anchoring draft describes how the browser picks an anchor node in the viewport and, when that node moves because of a change elsewhere, it compensates, so the visible content stays put: "It then queues an adjustment to the scroll position of y1 - y0, in the block flow direction, to be performed at the end of the suppression window." MDN notes that the behaviour is on by default in browsers that support it. The header is a sticky element, and the draft lists sticky and fixed elements as excluded from anchor selection, so the anchor is typically a node in the page body. When the header shrinks, the body moves up 24 px and the browser scrolls by the same amount in the opposite direction. In viewport coordinates the body has not moved, and the specification only counts a node as unstable if it moved in the viewport's coordinate space too. By this reading the shrink does not score. That is an inference from the two drafts, not a Chrome measurement. Chrome's metric changelog shows that shifts countered by scrolling were fixed in Chrome 90 (counterscroll) and Chrome 93 (scroll anchoring), but it does not describe a shrinking sticky header, so confirm the result in DevTools.

That reading has conditions, and each is a way for a real page to lose the protection. The draft selects no anchor when the scroller is at its origin ("If S is not scrolled away from the origin of its scrolling area in its block flow direction, then do not select an anchor node for S."). The property overflow-anchor: none on the body, or on a wrapper that holds the content, opts the subtree out. And the draft lists changes to height, min-height, margin, padding, position, transform and similar properties on elements between the anchor and the scroller as suppression triggers that cancel the adjustment. Where any of those apply, the shrink moves the body in viewport coordinates and it scores. The table below is that case: the header toggles every 0.4 s as a reader hovers around the threshold, with no hysteresis, and scroll anchoring off.

Header toggles within one window390x8441440x900
10.0240.010
20.0490.020
40.0970.041
60.1460.061
80.1950.082
110.2680.112

Each toggle is its own frame, and the gaps of 0.4 s sit under the 1 s limit, so they accumulate. Eleven toggles over 4 s, with anchoring off, reach 0.268 on the phone: poor. Whether any particular site gets there is something to confirm in DevTools, since the result depends on the browser's anchoring and the page's CSS.

The fixes. Do not change the header's layout height on scroll. Keep the header's box at one height and move or scale its contents with transform, or take the header out of the flow with position: fixed and give the body a fixed top padding equal to the header's tallest height. A header that hides on scroll down and returns on scroll up should do so with a transform translate, which the specification compares by starting points with and without transforms, so a pure transform change does not make the header or anything else unstable. If the shrink must change the layout, add hysteresis, so that the grow threshold sits well away from the shrink threshold and a reader cannot flicker across it.

Why one fix can leave the score unchanged: session windows

The page's CLS is the largest window total, not the sum of everything, so fixing a shift can leave the number where it was. This section builds a phone timeline from the rows above. The times are chosen for the illustration, and each shift keeps the score computed for it alone on the base layout; on a real page, an earlier shift moves the positions later ones start from, so treat the totals as approximate.

TimeEventScore
0.4 sHeader grows 24 px0.024
0.7 sHero image arrives without dimensions0.173
1.2 sCookie banner pushes the page0.185
1.9 sAd fills at y = 3000.175
2.8 sFAQ opens its first item after hydration0.094
8.0 sAd slot at y = 600 fills after a scroll0.079

The first five events are each less than 1 s apart and fit inside 5 s, so they form one window. The lazy ad at 8.0 s is a window of its own. The old definition, which summed every shift for the life of the page, would report 0.731. The current definition reports the largest window.

Fixes applied, cumulativeWindows (start: total)Page CLS
None0.4 s: 0.652; 8.0 s: 0.0790.652
Banner becomes an overlay0.4 s: 0.198; 1.9 s: 0.270; 8.0 s: 0.0790.270
Add image width and height0.4 s: 0.024; 1.9 s: 0.270; 8.0 s: 0.0790.270
Reserve the ad slot0.4 s: 0.024; 2.8 s: 0.094; 8.0 s: 0.0790.094
Set the FAQ state in HTML0.4 s: 0.024; 8.0 s: 0.0790.079
Reserve the header height8.0 s: 0.0790.079
Reserve the lazy adnone0

In the third row, fixing the image removed 0.173 of shift and the page score did not move, because the image was no longer in the biggest window. Removing the banner split one window into three, since the gap between the image at 0.7 s and the ad at 1.9 s is 1.2 s. After that the largest window was the ad plus the FAQ, and only fixing the ad moved the score. A fix ladder that ends in zero has to be walked in the order of the windows, starting with the window that holds the maximum, and any window total can be the next maximum.

The window rules cut both ways. If the same ad arrives at 2.9 s instead of 1.9 s, the first window holds the header, image and banner (0.382) and the second holds the ad and FAQ (0.270), so the page CLS is 0.382 against 0.652 with an unchanged total shift. A slow ad network can improve the score without improving the experience. The Chrome team knew this when it picked the window length. Its post on the change says: "For short windows, slower page loads and slower responses to user interactions could break layout shifts into multiple windows and improve the score." That is the reason for the 5 s cap on a window. It also limits the benefit from the other side: a feed that adds a 0.02 shift every 0.8 s for 12 s sums to 0.30 under the old definition, but the capped windows hold 0.14 each, so the page reads 0.14. A cap keeps long-lived pages from being punished for time on page and the 1 s gap keeps slow loads from being rewarded without limit.

A shift budget in pixels

The formula can be run backwards. For a given impact fraction, the biggest move that keeps one shift under a target is the target divided by the impact fraction, times the longer side of the viewport. That turns the 0.1 limit into pixels a designer can reason about.

Impact fractionMax move for 0.1, 390x844Max move for 0.25, 390x844Max move for 0.1, 1440x900Max move for 0.25, 1440x900
1.0084 px211 px144 px360 px
0.75113 px281 px192 px480 px
0.50169 px422 px288 px720 px
0.25338 px844 px576 px1440 px

An impact fraction of 1.00 is a push of everything on screen, which is what a banner above the header does. At 84 px a phone page is already at the limit, which fits the 168 px banner scoring 1.85 times the budget. A push that only reaches a quarter of the viewport can be 338 px and still pass.

Three working rules follow from the table, and each applies to the whole burst rather than to a single shift, since shifts in one window add.

  • Anything that can arrive after first paint and sits above most of the visible content needs reserved space or must be an overlay. The slack for a full-screen push on a phone is 84 px, less than the height of a standard mobile banner.
  • Content that arrives late and sits near the bottom of the first screen has a smaller impact fraction and a larger allowance, but not an unlimited one.
  • A design that is safe at 1440 px wide has about 1.7 times the pixel allowance of the same design at phone size for the same impact fraction. Sign-off on the desktop mockup does not carry to the phone.

Checking a hand calculation against DevTools and the API

A hand result is a prediction. Two quick checks confirm it. First, record a Performance trace in Chrome DevTools: web.dev's guide describes a Layout Shifts track in which clicking a shift shows its score and the shifted elements. Second, log the entries in the page itself with the Layout Instability API:

new PerformanceObserver((list) => {
  for (const e of list.getEntries()) {
    if (e.hadRecentInput) continue;
    const s = e.sources[0];
    console.log(e.startTime.toFixed(0), e.value.toFixed(3),
      s && s.node, s && s.previousRect, s && s.currentRect);
  }
}).observe({ type: 'layout-shift', buffered: true });

Each entry has the frame's value, the hadRecentInput flag and up to five sources, each with the node and its previous and current rectangles. To check an entry by hand, take the union of the previous and current rectangles of every source, clipped to the viewport, divide by the viewport area for the impact fraction, and divide the largest single movement by the longer viewport side for the distance fraction. The sources array is a sample of at most five nodes, so a hand union built from it can come out a little under the entry's value when more nodes moved.

Since Chrome 145, which reached stable users in February 2026, the rectangles are reported in CSS pixels, and the same release sorts the sources by impact area so that the first one is the biggest. Before that, the rectangles were in physical pixels and the order was arbitrary. Chrome's changelog for that release states that the scores themselves did not change. Lab and field numbers also differ for a structural reason: web.dev says lab tools typically load a page in a synthetic environment and so measure only shifts during load, while CrUX measures the whole life of the page. The late ad at 8.0 s in the timeline above is the sort of shift that appears only in the field.

Font swaps and the rest of the cause list

web.dev's list of common causes of poor CLS has four items: images without dimensions, ads, embeds and iframes without dimensions, dynamically injected content, and web fonts. The first three are the patterns above. Web fonts shift text by swapping a fallback font of different metrics for the real face, and the cure sits in font loading and fallback metrics, not in space reservation, so it is not repeated here. The web fonts troubleshooting guide covers font-display, preloading and the fallback adjustments, and the variable fonts guide has a section on fallback fonts and layout shift. The scoring is the same as above. A swap that rewraps a headline from two lines to three moves the text under it by one line height, and that move enters the same window as everything else on the page.

Embeds follow the ad rules. A video or map without a reserved box is an image without dimensions. The same applies to a social post widget whose height is unknown until it loads, which the Chrome team's threshold research gives as the reason it did not set the good limit at 0.05. The practical fix is a wrapper with a min-height or an aspect-ratio matching the embed's usual size.

What to specify in the design file

Every number in this guide is an input a designer owns, so the handoff is the place to settle them.

  • Image frames carry a ratio. Each image frame in the design is annotated with its aspect ratio, and the developer copies width and height from the exported file. A frame resized in the design tool without re-exporting is where the 4:3 against 16:9 mismatch comes from.
  • Late elements get a reserved box or an overlay, never "push". For a banner, notice, ad or embed, the design spec says which of the two it is. If it is a box, the spec gives its height at each breakpoint. Fill rate and geography decide the size of the box, so those inputs are part of the design brief.
  • Initial component state is a design decision. Which accordion item is open on load, which tab is selected and whether the nav is expanded are all drawn in the first frame, and that frame is what the server renders.
  • Header height is a token. One height for the layout, with any visual shrink done in the contents. Our landing page blueprint lists the vitals targets a page like this is held to, and the px, rem and em guide explains why a reserved height in rem grows with the reader's font size where a px height does not, which matters when the content inside it is text.
  • Budget the burst. Add up every late element on the first screen at phone size, using the pixel table above, before approving the design. Shifts that arrive less than 1 s apart add together, so the sum is the number to hold against 0.1.

Layout shift questions

How is Cumulative Layout Shift calculated?

Each rendered frame gets a layout shift score equal to the impact fraction times the distance fraction. The impact fraction is the area covered by moved elements in the previous and current frames, divided by the viewport area. The distance fraction is the largest move divided by the viewport's longer side. Scores in a burst less than 1 s apart, up to 5 s long, are summed, and CLS is the largest burst.

What layout shift score is good?

A CLS of 0.1 or less is good, and above 0.25 is poor. The page is judged at the 75th percentile of page loads, segmented across mobile and desktop, so a shift that affects a fifth of loads does not move the page's p75 but is still seen by those visitors.

Why is my CLS worse on mobile than on desktop?

The same pixel move is a larger share of a phone viewport. With a longer side of 844 px against 1440 px, and a text column that fills 92 percent of the phone's width, a 200 px push scores 0.140 on 390x844 and 0.046 on 1440x900 in the model used here.

Does a cookie banner at the bottom of the page affect CLS?

A banner that is fixed to the bottom of the viewport overlays content and moves no other element, so it scores 0. A banner that sits in the page flow above the content and appears after first paint pushes everything down and can score above the poor limit on a phone.

Do layout shifts after a click count?

Not if they happen within 500 ms of a discrete input such as a tap, click or key press. Shifts that start later, shifts caused by scrolling, and shifts caused by hovering do count.

Does an image with width and height still shift?

The attributes reserve space at their own ratio. If the file's real ratio differs, the text moves by the difference once the file loads, and a CSS rule that sets a conflicting height can do the same.

Specifications and web.dev pages cited

  • web.dev, Cumulative Layout Shift (CLS), page last updated April 12, 2023: formula, impact and distance fractions, the worked examples, session windows, the 0.1 and 0.25 thresholds, and the input exclusion; checked November 3, 2026.
  • web.dev, Optimize Cumulative Layout Shift, last updated February 7, 2025: images without dimensions, ads and late content, animations, and lab versus field; checked November 3, 2026.
  • web.dev, Evolving the CLS metric, published April 7, 2021: the session window decision and the 5 s cap; checked November 3, 2026.
  • web.dev, Best practices for cookie notices, last updated June 13, 2024: top-of-screen notices, sticky footers and modals; checked November 3, 2026.
  • web.dev, Defining the Core Web Vitals metrics thresholds, last updated May 7, 2025: the origin of the 0.1 and 0.25 limits; checked November 3, 2026.
  • web.dev, Web Vitals, last updated October 31, 2024: CLS listed as a stable Core Web Vital and the 75th percentile rule; checked November 3, 2026.
  • WICG, Layout Instability, Draft Community Group Report, 17 December 2025: unstable nodes, impact region, distance fraction, input exclusion and the 3 px note; checked November 3, 2026.
  • Chromium source tree, CLS metric changelog (GitHub mirror of chromium/src) with its Chrome 90, 93 and 145 notes: the latest entry, Chrome 145 in February 2026, states that scores did not change; checked November 3, 2026.
  • GoogleChrome/web-vitals, LayoutShiftManager.ts, main branch: the 1000 ms and 5000 ms session conditions; checked November 3, 2026.
  • Google for Developers, Google Publisher Tag: Minimize layout shift, last updated July 31, 2025: slot reservation, multi-size slots and fluid slots; checked November 3, 2026.
  • W3C CSS Working Group, CSS Scroll Anchoring Module Level 1, editor's draft: anchor selection, excluded subtrees and suppression triggers; checked November 3, 2026.
  • MDN, overflow-anchor, last modified July 26, 2026, and aspect-ratio, last modified July 21, 2026; checked November 3, 2026.