Subchapter 3.1
references/CLS.mdMarkdown4 KBView on GitHub
Read this reference when field CLS is poor, a performance trace reports layout shifts, or source inspection finds content that changes geometry without reserved space.
CLS scores unexpected shift clusters across a page visit. A layout-shift score is the impact fraction × distance fraction. Use the shifted-node and initiator evidence: the element that moved may be the victim of content inserted above it.
Bad:
<img src="photo.jpg" alt="Photo">
<iframe src="https://video.example/embed/123" title="Demo"></iframe>Good:
<img src="photo.jpg" alt="Photo" width="800" height="600">
<div class="video-frame">
<iframe src="https://video.example/embed/123" title="Demo"></iframe>
</div>.video-frame {
aspect-ratio: 16 / 9;
}
.video-frame iframe {
height: 100%;
width: 100%;
}Reserve a realistic minimum for ads and embeds whose final size can vary. A placeholder that later collapses can also shift content.
Do not insert banners, validation summaries, consent UI, or notifications above visible content without reserving space. Prefer an overlay, insert outside the active viewport, or allocate a stable container before the content arrives.
Bad:
main.prepend(notification);Good:
const slot = document.querySelector('[data-notification-slot]');
slot.replaceChildren(notification);The corresponding slot must already have appropriate reserved dimensions. Verify that responsive content and localization do not overflow it.
Use a fallback with similar metrics and tune it with size-adjust, ascent-override, descent-override, and line-gap-override when trace evidence attributes shifts to font replacement.
@font-face {
font-family: "Brand Fallback";
src: local("Arial");
size-adjust: 102%;
ascent-override: 92%;
descent-override: 24%;
line-gap-override: 0%;
}Do not copy these values to another font pair; derive them from the actual font metrics and test representative text.
Prefer transform and opacity for visual motion. Animating height, width, top, or left can trigger layout, but replacing them mechanically is not enough: confirm the transformed element does not obscure content or change the intended hit area.
.toast {
inset-block-start: 1rem;
inset-inline-end: 1rem;
position: fixed;
transform: translateY(-150%);
transition: transform 200ms;
}
.toast.is-visible {
transform: translateY(0);
}This observer reports shifts seen during the current page session. It is not the distribution of real visits.
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
console.log('Layout shift', entry.value);
entry.sources?.forEach(source => {
console.log('Shifted node', source.node);
console.log('Previous rect', source.previousRect);
console.log('Current rect', source.currentRect);
});
}
}
}).observe({ type: 'layout-shift', buffered: true });