CLS prevention
The five causes of layout shift, the font metric override that fixes most of it, and how to attribute a shift to the element that caused it.
Cumulative Layout Shift measures how much visible content moves without the user causing it. It is the vital most visible to a human — the tap that lands on the wrong button because an advert loaded — and the one most often "fixed" in the lab while remaining bad in the field, because the shifts happen at network speeds a lab run never sees.
Five causes, in the order we find them
The font fix that actually works
font-display: swap removes invisible text but guarantees a shift when the real font lands. The fix is to make the fallback occupy the same space as the web font, using the metric-override descriptors — then the swap is a change of shape rather than a change of layout.
@font-face {
font-family: 'Spectral Fallback';
src: local('Georgia');
size-adjust: 105.9%;
ascent-override: 92%;
descent-override: 24%;
line-gap-override: 0%;
}
body {
font-family: 'Spectral', 'Spectral Fallback', Georgia, serif;
}Attribute the shift to an element
A CLS score tells you there is a problem; attribution tells you which element caused it. Without that you are guessing, and the guess is usually wrong because the largest shift is rarely the one you noticed.
import { onCLS } from 'web-vitals/attribution';
onCLS(({ value, attribution: a }) => {
navigator.sendBeacon('/rum', JSON.stringify({
metric: 'CLS',
value,
element: a.largestShiftTarget, // the CSS selector that moved
time: a.largestShiftTime,
route: routePattern(),
}));
});Shifts that do not count, and one that should not happen
- A shift within 500ms of a user interaction is excluded — expanding an accordion is not penalised. This is why moving content in response to a tap is fine and moving it on a timer is not.
- Transform-based movement does not shift layout, so animate with transform rather than top or margin.
- CLS is measured across the whole session, not just the load. A late-loading footer widget or an infinite scroll that jumps can ruin an otherwise clean page — check the metric on long sessions, not only on first paint.