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.
Show the data as a table
| Pattern | 390x844 | 1440x900 |
|---|---|---|
| Image, no dimensions | 0.173 | 0.108 |
| Banner pushes the page | 0.185 | 0.035 |
| Ad, 300x250 at y 300 | 0.175 | 0.058 |
| FAQ opens after paint | 0.094 | 0.021 |
| Header grows at load | 0.024 | 0.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.
| Input | 390x844 (phone) | 1440x900 (desktop) |
|---|---|---|
| Viewport area | 329,160 px² | 1,296,000 px² |
| Longer side (distance denominator) | 844 px (height) | 1440 px (width) |
| Article column | 16 px gutters, 358 px wide | 720 px wide, centred |
| Column as a share of viewport width | 0.918 | 0.500 |
| Header height | 56 px, full width | 72 px, full width |
| Title block | 120 px | 140 px |
| Hero image, 16:9 at column width | 201.375 px tall | 405 px tall |
| Hero image top edge | 176 px | 212 px |
| Cookie banner height | 168 px | 88 px |
| Ad slot top edge (scored as a column of text below this y) | 300 px | 300 px |
| FAQ answer panel height | 150 px | 100 px |
| First FAQ question row, bottom edge (the panel opens below it) | 356 px | 356 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 = 300 | Impact | Distance | Score |
|---|---|---|---|
| 390x844 | 0.592 | 0.237 | 0.140 |
| 1440x900 | 0.333 | 0.139 | 0.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.
| Case | Viewport | Largest move | Impact | Distance | Score |
|---|---|---|---|---|---|
| No width or height, 16:9 file arrives | 390x844 | 201.4 px | 0.727 | 0.239 | 0.173 |
| No width or height, 16:9 file arrives | 1440x900 | 405 px | 0.382 | 0.281 | 0.108 |
| width and height set to the file's ratio | both | 0 px | 0 | 0 | 0 |
| Attributes say 4:3, file is 16:9 | 390x844 | 67.1 px up | 0.508 | 0.080 | 0.040 |
| Attributes say 4:3, file is 16:9 | 1440x900 | 135 px up | 0.157 | 0.094 | 0.015 |
| Image starts at y = 1400, below the fold | both | not in view | 0 | 0.239 / 0.281 | 0 |
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.
| Case | Viewport | Largest move | Impact | Distance | Score |
|---|---|---|---|---|---|
| Banner inserted above the header, pushes page down | 390x844 | 168 px | 0.929 | 0.199 | 0.185 |
| Banner inserted above the header, pushes page down | 1440x900 | 88 px | 0.580 | 0.061 | 0.035 |
| Same banner as a fixed overlay at the bottom | both | 0 px | 0 | 0 | 0 |
| Space reserved in the HTML, banner fills it | both | 0 px | 0 | 0 | 0 |
| Banner grows by animating height over 300 ms | 390x844 | 168 px over 18 frames | summed | summed | 0.163 |
| Banner grows by animating height over 300 ms | 1440x900 | 88 px over 18 frames | summed | summed | 0.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.
| Case | Viewport | Largest move | Impact | Distance | Score |
|---|---|---|---|---|---|
| No reservation, 300x250 arrives at y = 300 | 390x844 | 250 px | 0.592 | 0.296 | 0.175 |
| No reservation, 300x250 arrives at y = 300 | 1440x900 | 250 px | 0.333 | 0.174 | 0.058 |
| Same slot at y = 600 | 390x844 | 250 px | 0.265 | 0.296 | 0.079 |
| Same slot at y = 600 | 1440x900 | 250 px | 0.167 | 0.174 | 0.029 |
| Same slot at y = 1000, below the fold | both | not in view | 0 | 0.296 / 0.174 | 0 |
| 250 px reserved with min-height | both | 0 px | 0 | 0 | 0 |
| 100 px reserved, 250 px served | 390x844 | 150 px | 0.483 | 0.178 | 0.086 |
| 100 px reserved, 250 px served | 1440x900 | 150 px | 0.278 | 0.104 | 0.029 |
| 250 px reserved, slot shrinks to a 50 px creative | 390x844 | 200 px up | 0.537 | 0.237 | 0.127 |
| 250 px reserved, slot shrinks to a 50 px creative | 1440x900 | 200 px up | 0.306 | 0.139 | 0.042 |
| 250 px reserved, no ad fills, slot collapses | 390x844 | 250 px up | 0.592 | 0.296 | 0.175 |
| 250 px reserved, no ad fills, slot collapses | 1440x900 | 250 px up | 0.333 | 0.174 | 0.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 mix | Viewport | Share of loads that shift | Page CLS at p75 |
|---|---|---|---|---|
| 0 | 70% 300x250, 30% 320x50 | 390x844 | 100% | 0.175 |
| 50 | 70% 300x250, 30% 320x50 | 390x844 | 70% | 0.127 |
| 100 | 70% 300x250, 30% 320x50 | 390x844 | 70% | 0.086 |
| 250 | 70% 300x250, 30% 320x50 | 390x844 | 0% | 0 |
| 0 | 70% 300x250, 30% 320x50 | 1440x900 | 100% | 0.058 |
| 100 | 70% 300x250, 30% 320x50 | 1440x900 | 70% | 0.029 |
| 250 | 70% 300x250, 30% 320x50 | 1440x900 | 0% | 0 |
| 0 | 60% 970x90, 10% 728x90, 20% 320x50, 5% 728x250, 5% 300x250 | 1440x900 | 100% | 0.021 |
| 90 | same mix | 1440x900 | 10% | 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.
| Case | Viewport | Shift if counted | Recorded in CLS |
|---|---|---|---|
| Script opens the first item after paint, no input | 390x844 | 0.094 | 0.094 |
| Script opens the first item after paint, no input | 1440x900 | 0.021 | 0.021 |
| Tap opens the panel, 200 ms ease transition | 390x844 | 0.081 | 0 |
| Tap opens the panel, 200 ms ease transition | 1440x900 | 0.019 | 0 |
| Tap opens the panel, 800 ms linear transition | 390x844 | 0.080 | 0.028 |
| Tap opens the panel, 800 ms linear transition | 1440x900 | 0 (2.08 px per frame) | 0 |
| Tap opens the panel, 800 ms ease transition | 390x844 | 0.064 | 0 |
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.
| Case | Viewport | Largest move | Impact | Distance | Score |
|---|---|---|---|---|---|
| Header grows 24 px at load | 390x844 | 24 px | 0.857 | 0.028 | 0.024 |
| Header grows 32 px at load | 1440x900 | 32 px | 0.460 | 0.022 | 0.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 window | 390x844 | 1440x900 |
|---|---|---|
| 1 | 0.024 | 0.010 |
| 2 | 0.049 | 0.020 |
| 4 | 0.097 | 0.041 |
| 6 | 0.146 | 0.061 |
| 8 | 0.195 | 0.082 |
| 11 | 0.268 | 0.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.
| Time | Event | Score |
|---|---|---|
| 0.4 s | Header grows 24 px | 0.024 |
| 0.7 s | Hero image arrives without dimensions | 0.173 |
| 1.2 s | Cookie banner pushes the page | 0.185 |
| 1.9 s | Ad fills at y = 300 | 0.175 |
| 2.8 s | FAQ opens its first item after hydration | 0.094 |
| 8.0 s | Ad slot at y = 600 fills after a scroll | 0.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, cumulative | Windows (start: total) | Page CLS |
|---|---|---|
| None | 0.4 s: 0.652; 8.0 s: 0.079 | 0.652 |
| Banner becomes an overlay | 0.4 s: 0.198; 1.9 s: 0.270; 8.0 s: 0.079 | 0.270 |
| Add image width and height | 0.4 s: 0.024; 1.9 s: 0.270; 8.0 s: 0.079 | 0.270 |
| Reserve the ad slot | 0.4 s: 0.024; 2.8 s: 0.094; 8.0 s: 0.079 | 0.094 |
| Set the FAQ state in HTML | 0.4 s: 0.024; 8.0 s: 0.079 | 0.079 |
| Reserve the header height | 8.0 s: 0.079 | 0.079 |
| Reserve the lazy ad | none | 0 |
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 fraction | Max move for 0.1, 390x844 | Max move for 0.25, 390x844 | Max move for 0.1, 1440x900 | Max move for 0.25, 1440x900 |
|---|---|---|---|---|
| 1.00 | 84 px | 211 px | 144 px | 360 px |
| 0.75 | 113 px | 281 px | 192 px | 480 px |
| 0.50 | 169 px | 422 px | 288 px | 720 px |
| 0.25 | 338 px | 844 px | 576 px | 1440 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.