A page paints quickly, then a cookie-consent bar and a promo strip are injected at the top of the document a moment later, pushing the article down; the page's CLS is poor. Why does this pattern score so badly, and what would you change without removing either banner?
answer
- injected above the fold moves everything
- impact fraction near the whole viewport
- two bars, one session window
- out of flow displaces nothing
- transform entrance is free
basics
~20 sInjecting content above existing content moves nearly everything on screen, so the impact fraction approaches 1, and two injections seconds apart land in the same session window and add. Fix it by rendering the bars in the initial HTML, or taking them out of document flow.
solid answer
~50 sInjecting at the top of the flow is the worst possible shape for CLS: every visible element below the insertion point becomes an unstable element, so the impact fraction is close to the whole viewport, and the distance is the banner's own height. Two injections within a second of each other fall into one session window and their scores sum. The durable fix is to stop the layout from being wrong in the first place — render the bar in the server's initial HTML so the first layout the user sees already includes it, deciding server-side from the consent cookie whether it is needed. If it genuinely cannot be known before paint, take it out of the document flow: a `position: fixed` layer at the bottom of the viewport displaces nothing, and animating its entrance with `transform` records no shift at all. Reserving a fixed-height placeholder is the fallback, and it costs you that space whether or not the bar appears.
code
css · 11 lines.consent-bar {
position: fixed;
inset: auto 0 0 0;
transform: translateY(100%);
transition: transform 200ms ease-out;
will-change: transform;
}
.consent-bar[data-open="true"] {
transform: translateY(0);
}go deeper
Recognise the pattern: anything added above content already on screen pushes it down and costs you. Know that putting the bar in the page's initial HTML avoids the problem entirely.
Explain why top-of-flow injection maximises the impact fraction, and why two injections close in time combine into one session window rather than being scored separately.
Show judgment about the fix, not just the mechanism: prefer making the first layout correct, use an out-of-flow layer when the decision genuinely arrives late, and treat reserved space as a real cost. Be ready to attribute the shift to a specific script.
Own it as a policy problem. Decide who is allowed to inject into the document at runtime, require vendor content to render inside owned fixed-size or out-of-flow containers, and make that a condition on tag-manager and consent-vendor integrations rather than a per-page cleanup.
## Why top-of-document injection is the worst case A layout shift's score is driven by how much of the viewport moved and how far. Insert an element at the very top of the document flow and you have maximised the first factor by construction: **everything visible below it moves**. The unstable elements union to roughly the entire viewport, so the impact fraction sits near 1, and the score collapses to approximately the banner's height divided by the viewport's larger dimension. A 90 px bar on a phone is a substantial fraction of the screen — enough, on its own, to push a page past the commonly used 0.1 "good" guidance. Note the asymmetry with injecting *below* the fold: content added under the visible area moves nothing the user can see, and scores nothing. The problem is not injection, it is injection **above already-visible content**. ## Why two bars are worse than twice one bar Shifts less than a second apart fall into the same session window and their scores are summed. A consent bar at 800 ms and a promo strip at 1,300 ms are one burst, not two incidents. Worse, the second injection moves the same content again, from a position it had only just settled into — so you pay full impact twice. Staggering the injections further apart does not help the user at all; it only splits the score across windows, which is metric-gaming rather than a fix. ## Fix 1 — make the first layout correct The real defect is that the first layout the user saw was computed with information that later turned out to be wrong. The strongest fix is to have the information earlier. Consent state is usually in a cookie the server can read, so the server can emit the bar — or omit it — in the initial HTML. Then it is present in the very first frame, and there is no shift because there is no change. The same reasoning applies to promo strips driven by an experiment assignment: resolve the assignment server-side, or inline the decision in a blocking script in the document head, rather than letting a late client fetch rewrite the top of the page. ## Fix 2 — take it out of the flow When the decision genuinely cannot be made before paint, the next best answer is that the banner should not participate in layout at all. A `position: fixed` bar anchored to the bottom of the viewport overlays content instead of displacing it, so no element's start position changes and no shift is recorded. Bottom placement is also better UX on mobile — it is nearer the thumb and does not cover the headline — so this is rarely a sacrifice. If the bar must slide in, animate it with `transform: translateY(...)`, which moves pixels without moving layout and therefore scores zero. ```css .consent-bar { position: fixed; inset: auto 0 0 0; transform: translateY(100%); transition: transform 200ms ease-out; } .consent-bar[data-open='true'] { transform: translateY(0); } ``` ## Fix 3 — reserve the space, honestly The fallback is a placeholder of the banner's exact height rendered in the initial HTML, which the banner then fills. This works, and it is the right answer when the bar truly must sit in the flow at the top. Be clear-eyed about the cost: you have permanently spent that vertical space, above the fold, on something that may never appear. Reserve only when you expect the bar to appear for most users, and never guess the height — pin it with a fixed height on the container rather than letting the injected content define it. ## Third-party tags Often the injection is not yours: a tag manager, a consent vendor, or an ad script writes into the top of the page. The technique is the same but the enforcement is contractual — give the vendor a host container that you own, with a fixed height or fixed positioning, and require them to render inside it. If the vendor insists on writing directly to `document.body`, that is a procurement conversation, not a CSS one; measure the shift it causes and attribute it to that script by name. ## What not to do - **Do not delay the injection past the session window** to split the score. The user's experience is unchanged and you have made the jump happen later, when they are more likely to be mid-tap. - **Do not hide the bar behind a click** to claim the recent-input exemption. Shifts more than 500 ms after the input still count, and you have added a step for the user. - **Do not animate height or margin** to soften the entrance. Every animated frame that displaces content is its own layout shift, so a smooth 300 ms height animation produces a stream of shifts rather than one — softer to look at, no cheaper to score, and often worse. ## The rule to take away Never insert into the flow above content the user can already see. Either the content is in the first layout, or it lives in a layer that does not participate in layout.
- The team proposes fading the banner in over 300 ms so the jump feels gentler. Does that reduce CLS?Not if the fade is accompanied by the content actually moving. Opacity alone changes nothing about layout and is free, but if the banner is animating its height or margin, each animated frame displaces the content again and produces its own shift. Smoothness is a visual property; the metric only cares whether layout positions changed.
- Product insists the consent bar sits at the top of the flow. What is the best you can do?Render a container of the bar's exact final height in the server's initial HTML and let the bar populate it, so the first painted layout is already the final one. Fix the height in CSS rather than letting the injected content size it, and decide server-side from the consent cookie whether to emit the container at all so you do not spend the space needlessly.
- How would you prove which of the two bars is responsible for most of the score?Observe `layout-shift` entries and log each one's `value`, `startTime` and `sources` — the sources array names the nodes that moved with their previous and current rectangles. Correlating the timestamps with when each bar is inserted attributes the score precisely, which matters when one bar is yours and the other belongs to a vendor.
saying these in an interview costs you the question
- Suggests delaying the banner to dodge the session window
- Animates height or margin to make the jump smoother
- Assumes any injected element automatically counts as a shift
- Reserves space by guessing the banner's height
- Blames the third-party script without measuring its shift