Fixing Layout Shift From CSS
Some layout shift has nothing to do with slow images or adverts.
It comes from the stylesheet itself: styles that arrive after the first paint and rearrange what is already on screen, a web font that swaps in at a different size, a box with no reserved shape, or an animation that moves things by changing their position in the layout. Visitors see text jump just as they start reading. This guide covers only those CSS causes, how to prove which one you have in developer tools, and the specific rules that stop each one. Image, advert and embed shifts are covered in the separate CLS guides.
How CSS causes layout shift
Cumulative Layout Shift (CLS) is the Core Web Vital that measures unexpected movement. The browser records a layout shift whenever an element that was already visible changes its position in the layout between two frames, and scores it by how much of the viewport moved and how far. Google treats a CLS of 0.1 or less at the 75th percentile of page loads as good. Shifts that happen within half a second of a tap, click or key press are excluded, because the visitor expected something to change.
CSS is involved in almost every shift, but in this guide we mean cases where the stylesheet is the cause rather than just the place you apply the fix:
| CSS cause | What the visitor sees | Typical fix |
|---|---|---|
| A stylesheet or style block that applies after first paint | The page appears, then the layout rearranges | Complete critical CSS; no late layout rules |
| Web font swapping in with different metrics | Lines reflow and paragraphs grow or shrink | Metric-adjusted
fallback font, preload, font-display choice |
| Boxes whose height depends on content that arrives later | Everything below drops down | aspect-ratio,
min-height, contain-intrinsic-size |
Animations on top, height or margin | Content slides around without input | Animate transform and opacity instead |
| Classes added by JavaScript after load | Menus, sliders or headers snap into their real layout | Style the pre-script state to match |
| A scrollbar appearing once content is long enough | Centred content nudges sideways | scrollbar-gutter: stable |
The CSS Checker flags CSS-driven layout shift, where text and blocks jump as fonts or late styles arrive. For shifts from images without dimensions, adverts, cookie banners, embeds and dynamic content, see CLS Debugger Fixes.
Proving the cause in developer tools
Guessing at layout shift wastes time, because several causes can look the same. Capture the evidence first.
See the shifts as they happen
In Chrome developer tools, open the command menu (Ctrl+Shift+P or Cmd+Shift+P), type "rendering" and open the Rendering tab. Tick Layout Shift Regions. Reload the page and any area that moves flashes with a coloured overlay. Throttle the network to a slow profile in the Network panel first, because many CSS shifts only appear when fonts and stylesheets arrive late.
Find the element and the moment
Record a page load in the Performance panel. Layout shifts appear as markers on their own track; select one to see which nodes moved and their before and after positions. Line the marker up with the network track: if it sits just after a font file or stylesheet finishes downloading, you have found your cause.
Log shifts from the page itself
Paste this into the console before reloading, or add it temporarily to the page, to list every shift and the elements involved:
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.hadRecentInput) continue;
console.log('shift', entry.value.toFixed(4), 'at', Math.round(entry.startTime), 'ms');
for (const s of entry.sources || []) {
console.log(' moved:', s.node, s.previousRect, '->', s.currentRect);
}
}
}).observe({ type: 'layout-shift', buffered: true });
The logged node is the element that moved, which is not always the element that caused the move. If a paragraph moved, look at what is above it: a heading whose font swapped, a box that grew, or a header that changed position.
Late stylesheets and incomplete critical CSS
A stylesheet linked normally in the head blocks rendering, so it cannot cause a shift: the browser waits for it. Shifts start when CSS is made non-blocking to speed up first paint and the first paint is then drawn with incomplete styles. Common versions:
- Critical CSS that misses layout rules. The inlined rules style the header and hero but not the grid below, so the grid appears as a single column and then snaps into place when the full stylesheet arrives.
- Critical CSS generated at one screen size. A tool extracts rules for a desktop viewport, and phones get a first paint with the wrong layout.
- Style blocks injected late. A plugin, widget or CSS-in-JS library adds rules to the page after some content has rendered.
@importinside a deferred file. The imported file arrives even later and restyles again.
The test is simple: block the main stylesheet in the Network panel (right-click the request and choose Block request URL) and reload. What you see is your first paint. Anything that looks different from the final page above the fold, in size or position rather than just colour, will move when the stylesheet lands.
The fix is to make sure every rule that affects the size or position of above-the-fold elements is in the critical CSS, for each template and at phone and desktop widths. Colours, shadows and hover states can wait; widths, heights, display modes, grid templates, margins, padding, font sizes and line heights cannot. The render-blocking CSS guide covers generating critical CSS; here the point is that it must be complete for layout, not just for looks.
Font swaps and fallback metrics
When a web font has not arrived yet, the browser draws text in a fallback font. When the web font arrives, text is redrawn. If the two fonts have different widths, heights or line spacing, lines rewrap and every block below moves. That is a CSS problem with a CSS answer: make the fallback occupy the same space as the web font.
The @font-face descriptors size-adjust, ascent-override, descent-override
and line-gap-override let you define an adjusted version of a local font that closely matches
your web font's metrics:
@font-face {
font-family: "Brand Sans Fallback";
src: local("Arial");
size-adjust: 104%;
ascent-override: 92%;
descent-override: 24%;
line-gap-override: 0%;
}
body {
font-family: "Brand Sans", "Brand Sans Fallback", sans-serif;
}
The numbers above are placeholders; the right values depend on your font. Framework font tools such as the one built into Next.js calculate them automatically, and there are free generators that read a font file and output the descriptors.
The font-display descriptor decides what happens while the font loads. swap
shows fallback text immediately and swaps later, which is good for reading but can shift unless the fallback
is matched. optional uses the web font only if it is available almost immediately, otherwise
it keeps the fallback for that page view, which avoids the swap shift entirely at the cost of sometimes
not showing your font on a first visit. Preloading the one or two font files used above the fold makes
either option work better.
Font loading itself, including subsetting, hosting and preloading, is covered in depth in Fixing CLS from web fonts.
The fix, step by step
Step 1
Capture the shift and the element that moved
Turn on Layout Shift Regions in the Chrome Rendering tab, throttle the network, and reload each key template at phone and desktop widths. Record a load in the Performance panel and select each shift to see which elements moved and when. Run the layout-shift observer snippet in the console to log values and nodes. Match each shift to what finished loading just before it, such as a font, stylesheet or script, and write the pairs down.
Step 2
Make the first paint use the final layout
Block the main stylesheet in the Network panel and reload to see exactly what the first
paint looks like. Add every rule that controls size or position above the fold to your critical CSS,
including grid and flex settings, widths, heights, spacing, font sizes and line heights, for both phone
and desktop widths. Remove @import from deferred files, and stop plugins or scripts from
injecting layout rules after content has already appeared.
Step 3
Match the fallback font to the web font
Create an @font-face rule for a local fallback such as Arial or Georgia,
using size-adjust, ascent-override, descent-override and line-gap-override
values calculated for your web font, and list it straight after the web font in font-family.
Preload the font files used above the fold. Choose font-display: optional if avoiding any
swap matters more than always showing the brand font on first visit.
Step 4
Give every flexible box a shape in CSS
Any container whose size depends on something that arrives later needs a size before it
arrives. Use aspect-ratio for media, maps and video wrappers, keep height: auto
alongside width and height attributes on images, and set a min-height on slots filled by
scripts. Where you use content-visibility: auto on long sections, add contain-intrinsic-size: auto 600px
or a realistic estimate so the placeholder height is close to the real one.
Step 5
Animate with transform, not layout properties
Search your CSS for transitions and keyframes that change top, left,
margin, height, width or padding, especially ones
that run on load or on a timer, such as sliding banners and expanding notices. Rewrite them to use transform: translate()
or scale() and opacity, which move pixels without changing layout. Replace
transition: all with a list of the specific properties you intend to animate.
Step 6
Style the before-JavaScript state
Load
the page with JavaScript disabled or delayed and compare it with the finished page. Sliders that stack
every slide, menus that show expanded, and headers that switch from static to fixed all shift when scripts
add their classes. Write CSS for the initial markup so it already matches, for example showing only the
first slide and hiding the rest, and use position: sticky rather than toggling to fixed
on scroll.
Step 7
Re-measure in the lab and in the field
Repeat the throttled recordings and confirm the shifts you logged have gone or shrunk. Run the page through PageSpeed Insights and check the CLS value, then run the CSS Checker again to confirm the CSS-driven layout shift finding has cleared. Field data in Search Console updates over a rolling period of about four weeks, so note the date of the fix and check the report again once enough new visits have been recorded.
Reserving space with CSS
Several newer CSS properties exist specifically to give boxes a size before their content decides it.
aspect-ratio
.video-wrap { aspect-ratio: 16 / 9; width: 100%; }
.map-embed { aspect-ratio: 4 / 3; }
img, video { max-width: 100%; height: auto; }
Browsers turn an image's width and height attributes into an aspect ratio
automatically, as long as your CSS does not override the height with a fixed value. A stylesheet rule
such as img { height: 100%; } in a container with no height can undo that protection, so
check global image rules if images shift despite having dimensions.
contain-intrinsic-size with content-visibility
content-visibility: auto skips rendering off-screen sections, which speeds up long pages.
Without a size hint those sections are treated as zero height until they come near the viewport, so the
scrollbar and anything anchored below them can jump as they render. The auto keyword tells
the browser to remember the real size once it has been rendered:
.article-section {
content-visibility: auto;
contain-intrinsic-size: auto 800px;
}
scrollbar-gutter
On desktop systems with classic scrollbars, a page that starts short and becomes scrollable when content loads gains a scrollbar, and centred layouts nudge sideways. Reserving the gutter prevents that:
html { scrollbar-gutter: stable; }
height reserves space but can clip content that turns out
taller, for example a translated heading or larger text settings. Prefer min-height or aspect-ratio,
which reserve space without cutting anything off.Animations, sticky headers and state classes
An animation that changes top or margin moves the element in the layout,
and often pushes its neighbours. If it runs without the visitor doing anything, each frame can count
as a layout shift. Transforms move the painted element without changing layout, so they do not produce
layout shifts and are also cheaper for the browser.
/* Shifts content below on every frame */
.notice { transition: margin-top 0.3s; }
.notice.is-hidden { margin-top: -60px; }
/* Moves visually, layout stays put */
.notice { transition: transform 0.3s, opacity 0.3s; }
.notice.is-hidden { transform: translateY(-100%); opacity: 0; }
Sticky headers are a common hidden cause. A script that changes a header from normal flow to position: fixed
after some scrolling removes it from the layout, so all the content jumps up by the header's height.
Because this happens during scrolling, not after an input, it counts. position: sticky keeps
the header in the flow and avoids the jump; if you must use fixed, keep a placeholder of
the same height.
Also watch for a .js or .loaded class added to the root element after scripts
run, with rules that change layout. Anything keyed off such a class should change appearance only, not
size or position, or be matched by equivalent rules for the pre-script state.
1. Can CSS alone cause layout shift?
Yes. Stylesheets that are loaded without blocking and arrive after the first paint, web fonts that swap in with different metrics, animations that change an element's position in the layout, and rules keyed to classes added by JavaScript can all move content that visitors can already see. None of these involve images or adverts. The fix is usually a CSS change too: complete the critical CSS, match fallback font metrics, animate with transforms and style the pre-script state.
2. Does font-display swap cause layout shift?
It can. With swap, the browser shows text in a fallback font straight away and replaces it when the web font arrives. If the two fonts take up different amounts of space, lines rewrap and content below moves. The fix is not necessarily to abandon swap, but to make the fallback match the web font using size-adjust and the ascent, descent and line-gap override descriptors. Alternatively, font-display optional avoids the swap entirely on slow first visits.
3. Why does my page jump after the stylesheet loads?
Usually because the first paint was drawn with incomplete styles. This happens when the main stylesheet is loaded without blocking and the inlined critical CSS does not include every rule that affects layout above the fold, or was generated for a different screen size. Block the main stylesheet in developer tools and reload to see your real first paint. Any difference in size or position from the finished page is a shift waiting to happen.
4. Do CSS transforms count towards CLS?
No. Layout shift is measured from changes to an element's position in the layout, and transforms change how an element is painted without changing its layout position, so moving something with translate or scale does not create a layout shift entry. Animating properties such as top, left, margin, width or height does change layout, and if that happens without recent user input it can be counted. Use transform and opacity for any movement that runs automatically.
5. What is contain-intrinsic-size and when do I need it?
It gives an element a placeholder size while its content is not being rendered. It matters when you use content-visibility auto to skip rendering sections below the fold. Without it, those sections are treated as having no height, so the page length and anything below them can jump as they come into view. Setting contain-intrinsic-size with the auto keyword and a realistic estimated height lets the browser reserve space and then remember the true size.
6. Why is my CLS worse in Search Console than in Lighthouse?
Lighthouse measures a single simulated page load and stops. Search Console and PageSpeed Insights field data come from real Chrome users over a rolling period and include shifts that happen after load, while people scroll and interact. Sticky headers that switch to fixed, sections that render lazily, and content that expands on a timer often only show up in field data. Look for shifts during scrolling as well as during load when the two numbers disagree.
7. Should I use a fixed height to stop layout shift?
Only when you are certain of the content's size. A fixed height reserves space, but if the content turns out taller, because of longer text, a translation or a visitor's larger font setting, it overflows or gets clipped, which is an accessibility problem. Prefer min-height for slots filled later, aspect-ratio for media and embeds, and contain-intrinsic-size for deferred sections. These reserve space while still allowing the box to grow when it needs to.
The CSS Checker flags CSS-driven layout shift, where text and blocks jump as fonts or late styles arrive, alongside validation, render-blocking and unused CSS, specificity, prefix gaps and file size.
Open CSS Checker Fixes โ