Visual Stability
Content moves because something arrived after layout had already been decided. Reserve the space before the content exists, and the shift never happens.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
Why does content jump around while the page is loading, and what would have to be true for it not to?
A person starts reading, or reaches for a button. They expect the thing they are looking at to stay where it is — and to still be under their finger when it lands.
Layout shift is a cosmetic annoyance from images loading in. Add a fade-in transition so it looks smoother.
A fade does not stop the box from changing size. The content still moves; it moves while fading, which is harder to follow rather than easier.
- A fade does not stop the box from changing size. The content still moves; it moves while fading, which is harder to follow rather than easier.
- The most damaging shift is not visual at all — it is a tap landing on the wrong control because a banner was inserted above the button in the frame between the finger starting to move and arriving.
- A shift caused by a late font is not fixed by anything in the image pipeline, and a shift caused by an ad slot is not fixed by anything in the font pipeline. "Layout shift" is a symptom shared by half a dozen causes.
- Shifts often appear only on slow connections, which is exactly where they are never tested: locally everything arrives before first paint, so nothing has anywhere to move to.
- An accordion or a dropdown that pushes the page down is technically the same mechanism, and it is a real usability problem even though it was user-initiated and therefore excluded from the measurement.
What is actually happening
In the browser, not in the framework.
- Layout assigns every box a position and a size from the content and styles known at that moment. A shift happens when something arrives later that changes an already-painted box's geometry, and everything after it in flow moves (Normal Flow, Overflow and Margin Collapsing).
- Four things arrive late and change geometry: media without reserved dimensions, fonts with different metrics from the fallback, content injected above existing content, and containers sized by content that has not arrived.
- The field metric for this concern is cumulative layout shift, which scores unexpected movement by how much of the viewport moved and how far. It is cumulative because a page that shifts a little five times is as annoying as one that shifts a lot once.
- Shifts that follow a user interaction within a short window are treated as expected and excluded. That window is part of a definition maintained by the working group, which is a reason to know the rule and not a reason to memorise the interval.
- The browser also has scroll anchoring: when content is inserted above the viewport, it tries to keep the visible content in place by adjusting scroll position. It works well and it can be defeated, notably by scroll containers whose height changes under it (Scroll Restoration).
- An image with
widthandheightattributes gives the browser an aspect ratio before the bytes arrive, so it can reserve a correctly-proportioned box at the current column width. That is the whole mechanism behind the single most effective fix (Intrinsic Sizing and the Automatic Minimum).
What this makes the browser do
And which of it is avoidable.
- Every shift is layout and paint run again over content already on screen — work the browser has already done once and must throw away.
- A font swap invalidates layout for every element using that family, because line box heights and line-breaking depend on the metrics. On a text-heavy page that is close to a full-document layout (Style Invalidation).
- Scroll anchoring costs the browser bookkeeping on every insertion above the anchor, which is cheap and not free.
- Avoidable: essentially all of it. Reserving space converts re-layout into layout done once with the right numbers the first time.
What actually moves content
Every shift is the same event underneath: a box changed size or position after it had already been laid out, and the boxes after it in flow moved. What differs is what changed the box, and that determines the fix.
The table below is the domain's signature device applied to this problem. Note how many rows are maybe — whether a change costs layout depends on whether anything downstream of it is in flow, which is exactly why containment and absolute positioning change the answer.
| Change | style | layout | paint | composite | Why |
|---|---|---|---|---|---|
| Image loads with no reserved dimensions | yes | yes | yes | yes | The box goes from zero height to its intrinsic height, so every subsequent box in flow is repositioned and repainted. |
| Image loads with `width`/`height` or `aspect-ratio` | no | no | yes | yes | The box was already the right size; only its content is new. This is the fix, stated as a cost table. |
| Web font swaps in with different metrics | yes | yes | yes | yes | Line box heights and line breaking depend on font metrics, so text reflows wherever the family is used. |
| Web font swaps in with matched metrics | yes | maybe | yes | yes | Overrides make the fallback occupy the same space; layout may still run, but geometry does not change, so nothing visibly moves. |
| Banner inserted at the top of the flow | yes | yes | yes | yes | Everything below it moves down by the banner height. Scroll anchoring may compensate for the scroll position but not for a user mid-tap. |
| Same banner as a fixed overlay | yes | maybe | yes | yes | Out of flow, so nothing after it moves; it covers content instead, which is a different trade rather than no trade. |
| Accordion expands on click | yes | yes | yes | maybe | A real geometry change that the stability metric excludes because it followed input. Users still experience the jump. |
| Animating `transform` on a late element | no | no | no | yes | Composited: nothing in flow is disturbed. That is why it is the tool for motion that must not move neighbours. |
caveat The maybe rows depend on what is around the element: containment, absolute positioning, a fixed-height ancestor or a separate compositing layer can all stop a change from propagating. Read them as "layout may run, but whether anything visible moves depends on the surrounding structure" (CSS Containment).
Reserve the space before the content exists
The general principle is one sentence: the first layout should already be the final layout. Everything else follows. Anything whose size you can predict should have that size in the markup, and anything whose size you cannot predict should have a slot with a defensible default.
The version below is deliberately mundane. The interesting part is not the CSS; it is that the reservation lives in the HTML and the stylesheet rather than in a measurement taken after mount, which is a frame too late by construction.
<div class="card"></div>
/* height comes from whatever loads */
.card img { width: 100%; }
.card { min-height: 0; }
// after mount:
setHeight(ref.current.offsetHeight)<div class="card">
<img src="/thumb.avif" width="320" height="180" alt="" />
<p class="card__body"></p>
</div>
.card img { width: 100%; height: auto; aspect-ratio: 16 / 9; }
.card__body { min-height: 3lh; } /* three lines, whatever the font */The second version knows the aspect ratio and the text block's minimum height before a single byte of content arrives, so the first layout is already correct and nothing moves when the image decodes or the text renders. The first version cannot be correct on its first frame: the measurement it depends on does not exist until after that frame has been painted, so the correction *is* the shift.
The font is the one people miss
Image shifts get audited because they are obvious. Font shifts are missed because the page looks fine locally, where the font is cached, and because the movement is small on each line and large in aggregate down a long article.
The override descriptors below make the fallback occupy the same vertical space as the web font. The text still changes appearance when the real font arrives — that is unavoidable without hiding it — but the line boxes do not change height, so nothing below moves. Measure the ratios against your actual font rather than copying values.
1@font-face {2 font-family: "Inter";3 src: url("/inter-subset.woff2") format("woff2");4 font-display: swap;5 unicode-range: U+0000-00FF; /* subset: latin only */6}7 8/* A local fallback adjusted to match Inter's metrics, so the swap9 changes glyph shapes without changing line box heights. */10@font-face {11 font-family: "Inter Fallback";12 src: local("Arial");13 size-adjust: 107%;14 ascent-override: 90%;15 descent-override: 22%;16 line-gap-override: 0%;17}18 19body { font-family: "Inter", "Inter Fallback", sans-serif; }Three separate decisions are visible here. font-display: swap chooses readable fallback text over invisible text. unicode-range ships only the glyphs this page needs. The second @font-face block is the one that removes the shift — without it, swap guarantees a reflow rather than preventing one.
How to build it
Most important first.
- Give every image and video
widthandheightattributes, or a CSSaspect-ratio. This is the highest-value single change in the lesson and it costs nothing (Images, Video and the Elements That Own Their Layout). - Reserve space for anything with unknown content — ad slots, embeds, third-party widgets — with a
min-heightthat matches the common case, and accept that the uncommon case will still move. - Insert new content below the fold or into space already reserved for it. Banners, notification bars and cookie notices belong in an overlay or a pre-reserved slot, never pushed into the top of the flow (Positioning and Stacking Contexts).
- Match the fallback font's metrics to the web font using
size-adjust,ascent-overrideanddescent-override, so the swap changes glyph shapes without changing line boxes (Images and Fonts). - Make skeletons the same size as the content that will replace them. A skeleton that is shorter than the real thing converts one shift into two.
- Animate
transformandopacityfor motion that must not disturb layout. They are composited and do not move anything else in flow; this is a mechanism, not a blanket rule, and it applies to this problem specifically (Cheap and Expensive Animation). - Reserve space in the server-rendered HTML, not in JavaScript. A measurement made after hydration is a frame too late (Hydration).
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- Shifting content is a serious problem for screen-magnifier users: they are looking at a small window onto the page, and a shift moves their view to somewhere they did not choose.
- For motor-impaired users and anyone using a switch, a control that moves between target acquisition and activation is not a nuisance but a mis-click, sometimes a destructive one (Keyboard Operability).
- A screen-reader user's reading position can be disrupted when the DOM changes around it. Insert content in a way that does not reorder what is already there, and announce it deliberately with a live region if it matters (Live Regions and Announcement).
- Text that is invisible while a font loads is unreadable for everyone and is not announced any differently; it is a content-availability problem, not a styling one.
- Respect
prefers-reduced-motionfor any transition used to soften a change. Motion used to mask a layout change is exactly the motion some users have asked not to see (Contrast, Colour and Motion).
What can go wrong
- Reserving the wrong amount of space, which produces a smaller shift plus a visible gap — often worse than the original because it is permanent.
- Using
aspect-ratioon an element whose intrinsic size the browser already knows, and overriding it into a different shape. - Fixing shifts by making late content
position: absolute, which removes it from flow and can cause it to overlap real content instead of moving it. - A layout that depends on a measurement taken in JavaScript after mount: the first frame is wrong by construction, and the correction is the shift (What a Mutation Costs).
- Third-party embeds that resize themselves after load. You can reserve their initial size and you cannot control their second one (Third-Party Scripts and the Supply Chain).
- Suppressing the metric rather than the movement — for example by keeping content invisible longer — which trades a visual-stability problem for a loading one (Loading: Why Content Arrives Late).
- A font and the text it styles race. If the font wins, nothing moves; if the text is painted first, the swap re-lays it out (Images and Fonts).
- A late-arriving advert and the user's scroll race: content inserted just as the user reaches for something is the worst case, and it is the common case.
- A JavaScript measurement and the first paint race. On a fast machine the measurement usually wins and the bug never appears locally.
- Content injected above a control at the moment of a click is the mechanism behind clickjacking-adjacent interface manipulation. When it is deliberate rather than accidental, it is an attack (Clickjacking and Framing).
- Third-party scripts that inject or resize content control your layout stability. Framing them, giving them a fixed slot, or loading them into a sandboxed frame limits what they can move (Third-Party Scripts and the Supply Chain).
- A consent banner that appears late and pushes the page down is both a stability problem and a consent-quality problem: users click whatever is under their finger.
- The browser enforces nothing about layout stability. Nothing here is a security control; it is a design obligation.
- "Layout shift is cosmetic." It is the cause of mis-taps on destructive controls, and it is one of the few performance problems users can describe precisely.
- "Setting
widthandheightis obsolete now that layout is responsive." They are how the browser learns the aspect ratio; CSS still controls the rendered size. The attributes and responsive layout are not in conflict. - "Only images cause it." Fonts, injected banners, dynamically-sized containers and third-party embeds all do, and font-driven shift is the one that most often survives an image audit.
- "A transition fixes it." A transition changes how the movement looks. The box still ends up somewhere different and the content below it still moves.
- "Our score is fine so we have no problem." The metric excludes movement that follows user interaction, and an accordion that jumps the page still frustrates people (Accessible Component Patterns).
Measuring it, and what changes in the field
- Field data for the stability concern, with attribution to the element that moved — the element name is usually the entire diagnosis (Vitals in the Field).
- Layout-shift records in the browser, which report the score, the affected elements and whether the shift followed recent input (Real User Monitoring).
- The Performance panel's layout-shift markers on the timeline, matched against what was arriving at that moment (Debugging Rendering and Jank).
- A throttled connection with an empty cache, which is the only local configuration in which shifts reliably reproduce.
- Visual regression testing across breakpoints, which catches reserved space that is right at one width and wrong at another (Visual Regression Testing).
- On a fast connection almost everything arrives before first paint and there is nothing to shift. Stability problems are disproportionately a slow-network phenomenon, which is why they survive local testing.
- On a narrow viewport, a reserved box sized for a desktop column is the wrong height, so a fix that works at one breakpoint creates a gap or a shift at another (Media Queries Beyond Width).
- With a slow device, the gap between first paint and hydration widens, and anything sized by JavaScript after mount has longer to be visibly wrong (The Real Cost of JavaScript).
- In a long list being appended to during scroll, insertion above the viewport is constant, and scroll anchoring is doing continuous work you should not defeat (List Virtualization).
- Reserved space that is wrong is a permanent gap in exchange for a transient shift. It is usually the right trade and it is a trade.
- Matching fallback metrics to a web font takes real effort per family and per weight, and the fallback text will look slightly wrong in exchange for not moving.
- Holding content back until everything has arrived removes shifts by making the page slower to be useful — trading one concern for another rather than fixing anything (Loading: Why Content Arrives Late).
- Overlaying late content instead of inserting it avoids the shift and covers something. That is a design decision with its own accessibility cost.
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERALThe mechanism — a box's geometry changing after it has been painted, moving everything after it in flow — follows from how layout works in every engine. The reserved-space fixes are equally universal.
- SPEC-EVOLVINGThe stability metric named here has had its definition revised, including how it treats long-lived sessions, and its rating boundaries are published by the web vitals working group rather than by the platform. The concern is durable; the score, the window it uses and the boundaries are not.
- BROWSER-SPECIFICScroll anchoring and the layout-shift record are implemented across engines with differences in detail — which insertions are anchored, and how a shift is attributed to an element — so a score measured in one browser is not directly comparable to another's.
- NETWORK-SPECIFICShifts are largely a slow-connection phenomenon: on a fast link most resources arrive before first paint and there is nothing left to move, which is why this class of bug survives testing on a developer's network and appears in field data.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Software Design — reserving space is an instance of designing for the state you will be in before the data arrives, rather than treating the loaded state as the only real one.