Skip to content

Layout Shift

You go to press a button, and just before you click, something loads above it and the button slides away. You hit whatever moved into its place.

Anything that arrives late should land in space that was already waiting for it.

Post moved
0px
Missed clicks
0

MC

Mira Chen

2m ago

Morning light at the pond.

Click Post. Late content arrives as you aim.

In the jumpy card, a photo, a banner and an error each push Post down. In the reserved card, the photo fills a box that was there from the start, the banner floats over the post, and the error has its own line.

Where shifts come from

  • Images and video: set width and height, or an aspect-ratio, so the browser can size the box before the file arrives.
  • Async content: give the skeleton the final dimensions, or the container a fixed height.
  • Validation messages: reserve the line under the field, as in input details.
  • Conditional buttons and badges: keep their slot, or overlay them. A number that changes width as it ticks is a small shift too, which tabular numbers fixes.
  • Scrollbars: on Windows, a scrollbar appearing narrows the page and everything centered jumps sideways. scrollbar-gutter: stable keeps room for it.

Web fonts

With font-display: swap, text renders first in a fallback font, then in the web font. If the two have different widths, lines rewrap and everything below them moves.

A slow afternoon in the water garden

Monet painted the same pond about 250 times, from the bridge, from the bank and from a small boat, chasing the light.

Continue reading
Showing
Inter
Link moved
-

Both fall back to Arial. Plain Arial is narrower, so the paragraph loses a line and the link jumps 24px when Inter arrives. The adjusted one scales Arial with size-adjust: 107.89% and matches its ascent and descent, so nothing moves. next/font generates these overrides for every font it loads.

CLS

Chrome measures this as Cumulative Layout Shift, one of the Core Web Vitals. 0.1 or less is good, over 0.25 is poor. Shifts within 500ms of a click or key press don't count, so the error that pushes Post down right after you press it costs nothing in CLS. It still costs the person pressing again.

Usage

app.tsx
<img src="/pond.jpg" width="1200" height="600" class="h-auto w-full" alt="" />

<div class="aspect-2/1 rounded-lg bg-muted">...</div>

<p class="h-5 text-sm text-red-600">{error}</p>

<html class="[scrollbar-gutter:stable]">
img {
  height: auto;
}

.photo {
  aspect-ratio: 2 / 1;
}

.field-error {
  height: 1.25rem;
}

html {
  scrollbar-gutter: stable;
}

@font-face {
  font-family: "Inter Fallback";
  src: local("Arial");
  size-adjust: 107.89%;
  ascent-override: 89.79%;
  descent-override: 22.36%;
  line-gap-override: 0%;
}

body {
  font-family: "Inter", "Inter Fallback", sans-serif;
}

Safari supports size-adjust but not yet the ascent and descent overrides. Give text an explicit line-height and the line boxes stay the same height there anyway.

Resources

Search concepts

Search for a command to run...