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.
insert before content- Viewport height
- --
- Banner before / after
- --
- Target movement
- --
reserve 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
- Optimize Cumulative Layout Shift - avoid inserting content without interaction - Google web.dev, checked 2026-08-06
- Understanding Focus Not Obscured (Minimum) - W3C Web Accessibility Initiative, checked 2026-08-04
- Understanding Focus Order - W3C Web Accessibility Initiative, checked 2026-08-04
Formal evidence
Browser and viewport matrix
Cells show failing / fixed target movement in CSS pixels.
| Browser | 320 | 375 | 768 | 1024 | 1440 |
|---|---|---|---|---|---|
| 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-320128pxchromium-fixed-movement-3200pxchromium-horizontal-reflow-3200px / 0pxchromium-layout-instability-api-3200.10218750000000001 / 0chromium-consent-keyboard-path-320true/true / true/truechromium-failing-movement-375128pxchromium-fixed-movement-3750pxchromium-horizontal-reflow-3750px / 0pxchromium-layout-instability-api-3750.08074074074074074 / 0chromium-consent-keyboard-path-375true/true / true/truefirefox-failing-movement-320128pxfirefox-fixed-movement-3200pxfirefox-horizontal-reflow-3200px / 0pxfirefox-consent-keyboard-path-320true/true / true/truefirefox-failing-movement-375128pxfirefox-fixed-movement-3750pxfirefox-horizontal-reflow-3750px / 0pxfirefox-consent-keyboard-path-375true/true / true/truewebkit-failing-movement-320128pxwebkit-fixed-movement-3200pxwebkit-horizontal-reflow-3200px / 0pxwebkit-consent-keyboard-path-320true/true / true/truewebkit-failing-movement-375128pxwebkit-fixed-movement-3750pxwebkit-horizontal-reflow-3750px / 0pxwebkit-consent-keyboard-path-375true/true / true/true
Run 2026-08-04T11-36-57-234Z. chromium 149.0.7827.201, firefox 153.0, webkit 26.5. Open raw manifest