All cases
RL-015 / Layout ShiftEvidence verified

Add a Cookie Banner Without Shifting or Obscuring Content

A project-authored consent notice compares unreserved insertion with a stable region, then verifies that keyboard dismissal removes the notice and returns focus to the prior control.

Experiment mode

Fixture viewport

Failinginsert before content
Viewport height
--
Banner before / after
--
Target movement
--
Fixedreserve 128px region
Viewport height
--
Banner before / after
--
Target movement
--

Three-browser evidence verified. Live values come from your current browser. The formal matrix records the controlled browser runs and fixture revision used for the published conclusion.

Observed failure

The notice is inserted before content without a region

The failing fixture adds a 128-pixel notice above the main task after initial layout. The focused task moves down by the notice height.

Minimal code diff

Allocate a stable region and preserve the keyboard path

The fixed fixture reserves the notice height before insertion. Its real Reject and Accept buttons dismiss the fixture notice and return focus to the task control that preceded it.

Before

.consent-slot { min-block-size: 0; }

After

.consent-slot { min-block-size: 128px; }
dismissButton.onclick = () => previousTarget.focus();

Root cause

Automatically inserting a notice at the top of normal flow increases the document height above the current task. The result is unexpected movement. Moving the same notice to a fixed overlay can avoid that movement, but it introduces a different risk if the overlay hides content or keyboard focus.

This fixture chooses a third approach: a preallocated normal-flow region. It avoids overlap and gives the banner a predictable reading and focus position. The fixed height is specific to the controlled English fixture and must not be copied to notices with different text, localization, controls, or legal requirements.

Focus behavior

The notice does not steal focus when it appears. Both actions are native buttons. When either action dismisses the fixture, the script removes the notice and restores focus to the task control that was active before insertion. This behavior is tested for keyboard continuity; it does not store a consent decision.

Measurement support

Every browser records the notice region’s height before and after insertion, the task control’s movement, and the result of a programmatic keyboard-path dismissal. Chromium additionally records non-input Layout Instability API entries for insertion.

When this fix is not enough

A production consent interface needs requirements based on actual jurisdictions, vendors, purposes, and data flows. Long translations can exceed a fixed region. Modal implementations require a complete dialog focus model. Fixed overlays require separate obstruction checks at every supported viewport.

Regression condition

At 320 and 375 CSS pixels, the failing insertion must move the task by 128 pixels. The fixed region must remain 128 pixels before and after insertion and produce zero target movement. In the fixed state, activating a notice action must remove the notice and return focus to the task control. Both states must have zero horizontal page overflow.

Primary references

Sources checked for this case

  1. Optimize Cumulative Layout Shift - avoid inserting content without interaction - Google web.dev, checked 2026-08-06
  2. Understanding Focus Not Obscured (Minimum) - W3C Web Accessibility Initiative, checked 2026-08-04
  3. Understanding Focus Order - W3C Web Accessibility Initiative, checked 2026-08-04

Formal evidence

Browser and viewport matrix

Three-browser run

Cells show failing / fixed target movement in CSS pixels.

Browser32037576810241440
Chromium+128 / +0+128 / +0+128 / +0+128 / +0+128 / +0
Firefox+128 / +0+128 / +0+128 / +0+128 / +0+128 / +0
WebKit+128 / +0+128 / +0+128 / +0+128 / +0+128 / +0
26/26 recorded assertions passed
  • chromium-failing-movement-320128px
  • chromium-fixed-movement-3200px
  • chromium-horizontal-reflow-3200px / 0px
  • chromium-layout-instability-api-3200.10218750000000001 / 0
  • chromium-consent-keyboard-path-320true/true / true/true
  • chromium-failing-movement-375128px
  • chromium-fixed-movement-3750px
  • chromium-horizontal-reflow-3750px / 0px
  • chromium-layout-instability-api-3750.08074074074074074 / 0
  • chromium-consent-keyboard-path-375true/true / true/true
  • firefox-failing-movement-320128px
  • firefox-fixed-movement-3200px
  • firefox-horizontal-reflow-3200px / 0px
  • firefox-consent-keyboard-path-320true/true / true/true
  • firefox-failing-movement-375128px
  • firefox-fixed-movement-3750px
  • firefox-horizontal-reflow-3750px / 0px
  • firefox-consent-keyboard-path-375true/true / true/true
  • webkit-failing-movement-320128px
  • webkit-fixed-movement-3200px
  • webkit-horizontal-reflow-3200px / 0px
  • webkit-consent-keyboard-path-320true/true / true/true
  • webkit-failing-movement-375128px
  • webkit-fixed-movement-3750px
  • webkit-horizontal-reflow-3750px / 0px
  • webkit-consent-keyboard-path-375true/true / true/true