Three developers comparing webpage layouts and CSS fixed overlay positions on a large monitor in an office.

Why a Fixed Overlay Scrolls With Its Transformed Parent

Fri, Oct 30, 2026

An overlay can still compute to position: fixed and nevertheless move when the document scrolls after a wrapper receives transform: translateX(0px). That is the regression this playbook qualifies. The product requirement is narrower than “the element is fixed”: the ordinary overlay must remain at viewport top-left while document scroll moves through 0, 80 and 200 CSS pixels. If the box instead tracks a non-root ancestor, the declaration is not necessarily invalid; the reference rectangle has changed.

That distinction matters because the CSS positioning scheme and the chosen containing block are separate decisions. A non-none transform can make an ancestor the containing block for fixed descendants, and will-change: transform can establish the same layout consequence even while the computed transform is none. A large z-index can alter stacking behavior without restoring viewport-relative coordinates. The correct repair therefore depends on measured ownership, not on the word fixed or on visual intuition.

The evidence below is a supplied preparation/reference run from 2026-10-30: Node v24.22.1, Playwright 1.62.1, explicitly selected Chrome for Testing 144.0.7559.96 on Linux x64, at a 900×500 CSS-pixel viewport and device scale factor 1. It tests nine named cases and 36 measured states. The goal is operational: ACCEPT_GEOMETRY only when the measured rectangle matches the declared reference rectangle, REPAIR when viewport anchoring is required but ancestor styling or DOM placement changes ownership, and HOLD when the owner, actual scroll, relevant ancestor properties or supported-browser evidence is missing.

Declare which rectangle the overlay should follow

Start with the product contract, not with a CSS property name. In this fixture, the overlay is an ordinary div with position: fixed; top: 0; left: 0; width: 100px; height: 36px. The intended contract is simple: its border box must remain at {left: 0, top: 0} while the document scrolls vertically. That viewport-relative rectangle is the acceptance oracle.

An ancestor-relative result is therefore a measurable mismatch, even when the browser is following the positioning rules correctly. If the overlay's top becomes 160, then 80, then -40 at document scroll 0, 80, then 200, it is moving with an ancestor whose authored vertical origin is 160 CSS pixels. The important diagnosis is not “fixed stopped working.” It is “the fixed-position containing block is not the viewport required by this product contract.”

This article deliberately does not reuse the workflow in Refonte Learning's sticky scroll-container diagnosis. Sticky positioning has different questions about the nearest scrollport, inset activation and the containing-block boundary. Here, ancestor overflow remains visible, there is no ancestor scrolling, and the only scrolling system under test is the document viewport.

The decision rule follows from that contract. If the overlay remains at viewport origin under the recorded scroll values, geometry can be accepted for this scope. If it follows a transformed or will-change ancestor when the product requires viewport anchoring, repair the owning style or DOM placement. If the intended owner is unknown, or if the actual scroll and relevant computed properties were not recorded, do not infer success from a screenshot: HOLD.

Identify the containing block selected for a fixed descendant

The core rule comes from two CSSWG Editor's Drafts, which are rolling work in progress rather than browser launch announcements. The 14 May 2026 CSS Positioned Layout Module Level 3 says a fixed box uses the nearest ancestor that establishes a fixed-position containing block; if no such ancestor exists, continuous media uses the layout viewport as the initial fixed containing block. A viewport-attached fixed box does not move with document scrolling.

The 30 November 2025 CSS Transforms Module Level 1 adds the transform-specific rule used here: for CSS box-model elements, any computed transform other than none establishes a containing block for descendants, including fixed-position descendants, using the ancestor's padding box. The same non-none transform also creates a stacking context, but those are separate effects.

In this fixture the wrapper has no border or padding, so its padding-box origin coincides with the simple authored geometry: 160 CSS pixels down from the top because of the preceding spacer, and 60 CSS pixels from the left because of margin-left: 60px. That intentionally removes edge complications from the containing-block calculation.

Separate fixed positioning from viewport anchoring

position: fixed names the positioning scheme; it does not, by itself, promise viewport attachment. The containing-block chain decides the reference rectangle. When no qualifying ancestor intervenes, the viewport supplies the fixed-position reference in this desktop continuous-media experiment. When a qualifying transformed ancestor intervenes, the same position: fixed declaration is resolved against that ancestor instead.

That is the narrow mental model to preserve. Root/body transforms, mobile visual viewports and paged media can introduce different questions and are outside this laboratory.

Keep geometry separate from paint order

A stacking context affects painting relationships; a containing block affects coordinate resolution. C02 gives the first negative control: position: relative; z-index: 1 on the wrapper leaves the fixed overlay at {0,0} for all three document scroll offsets. Later, C05 raises the overlay's own z-index to 2147483647 while keeping the wrapper transformed; its coordinates still follow the ancestor.

Those controls prevent two common category errors. A stacking context is not automatically the transform-specific fixed containing block in this fixture, and a high overlay z-index does not rewrite the coordinate system. Correct geometry also does not prove visibility, occlusion, hit testing or interaction quality.

For review purposes, treat the containing-block decision as a coordinate-system selection step that occurs before you argue about paint order. Ask first, “which box supplies zero for top: 0; left: 0?” Only after that answer is established should stacking questions enter the discussion. This sequencing keeps remediation bounded: a coordinate mismatch needs an ownership repair, whereas an occlusion problem needs separate paint-order evidence.

Freeze a minimal layout with independent expected coordinates

A reproducible regression test needs expected values that do not derive from the observation under test. The fixture therefore authors literal dimensions: viewport 900×500, a 160-pixel #before spacer, a wrapper at margin-left: 60px with width 360px and height 280px, a 100×36 fixed overlay, and a 1000-pixel #after block to provide document scroll range. Body and root margin/padding are zero. The document is sampled at requested vertical offsets 0, 80 and 200.

Before measuring anything, the expected origins are known. A viewport-bound overlay is always left = 0, top = 0. An ancestor-bound overlay begins at wrapper origin left = 60, top = 160; document scrolling moves that ancestor-relative client rectangle to 160 - scrollY, producing 160, 80, -40. C04 adds an authored translateY(20px), so its expected top sequence becomes 180, 100, -20 while left remains 60.

That independence is important. The wrapper's own getBoundingClientRect() is logged because it is diagnostically useful, but it never becomes the expected answer for the overlay. Otherwise a wrong ancestor reference could be “validated” by comparing one observation with another observation produced by the same layout state.

This is also distinct from native CSS anchor positioning, which solves declarative placement relative to an anchor and may involve fallbacks or collision behavior. No anchor property appears here. The question is only which rectangle an ordinary fixed descendant uses under a small set of ancestor states.

Run the complete nine-case geometry fixture

The laboratory is one offline HTML string driven by Playwright. A reader only needs basic DOM and CSS familiarity; Refonte Learning's HTML and CSS foundations is prerequisite-level context, not authority for the fixed-position rules.

Use a fresh writable Node project and install the exact Playwright package version used by the preparation run:

npm install --save-exact [email protected]

Save the following listing as css_fixed_fixture.cjs beside that project's node_modules, preserving LF newlines and the final newline. Its supplied SHA-256 is 146c9890361c74dfcee4c849b53982763ad4a6cf6f1bb0b73bb31aaafab547e9. The digest identifies these source bytes; it is not a correctness certificate.

const { chromium } = require('playwright');
const fs = require('node:fs');
const crypto = require('node:crypto');

function requireFact(ok, message) {
  if (!ok) throw new ErrorHOLD: ${message});
}
const HTML = <!doctype html><html><head><meta charset="utf-8">
<title>Owned fixed-position geometry fixture</title><style>
html, body { margin: 0; padding: 0; scroll-behavior: auto; }
#before { height: 160px; }
#wrap { margin-left: 60px; width: 360px; height: 280px; }
#overlay { position: fixed; top: 0; left: 0; width: 100px; height: 36px; }
#after { height: 1000px; }
</style></head><body><div id="before"></div><div id="wrap">
<div id="overlay">Owned overlay</div></div><div id="after"></div></body></html>;

// Expected origins are fixture constants, never measured ancestor rectangles.
const CASES = [
  {id:'C01_neutral', style:{}, final:[0,0], fixedTo:'viewport'},
  {id:'C02_relative_zindex', style:{position:'relative',zIndex:'1'}, final:[0,0], fixedTo:'viewport'},
  {id:'C03_identity_transform', style:{transform:'translateX(0px)'}, final:[60,160], fixedTo:'ancestor'},
  {id:'C04_translated_ancestor', style:{transform:'translateY(20px)'}, final:[60,180], fixedTo:'ancestor'},
  {id:'C05_high_overlay_zindex', style:{transform:'translateX(0px)'}, overlayZ:'2147483647', final:[60,160], fixedTo:'ancestor'},
  {id:'C06_will_change_only', style:{willChange:'transform'}, final:[60,160], fixedTo:'ancestor'},
  {id:'C07_remove_transform', style:{transform:'translateX(0px)'}, action:'remove', before:[60,160], final:[0,0], fixedTo:'viewport'},
  {id:'C08_remove_but_keep_will_change', style:{transform:'translateX(0px)',willChange:'transform'}, action:'remove', before:[60,160], final:[60,160], fixedTo:'ancestor'},
  {id:'C09_reparent_to_neutral_body', style:{transform:'translateX(0px)'}, action:'reparent', before:[60,160], final:[0,0], fixedTo:'viewport'},
];

async function measure(page, c, origin, owner, phase) {
  const samples = [];
  for (const y of [0,80,200]) {
    const observed = await page.evaluate(async target => {
      window.scrollTo(0,target);
      await new Promise(requestAnimationFrame);
      await new Promise(requestAnimationFrame);
      const wrap = document.querySelector('#wrap');
      const overlay = document.querySelector('#overlay');
      const ws = getComputedStyle(wrap), os = getComputedStyle(overlay);
      const r = overlay.getBoundingClientRect(), wr = wrap.getBoundingClientRect();
      return {scrollX:scrollX,scrollY:scrollY,top:r.top,left:r.left,width:r.width,height:r.height,
        wrapperTop:wr.top,wrapperLeft:wr.left,position:os.position,insetTop:os.top,insetLeft:os.left,
        transform:ws.transform,willChange:ws.willChange,wrapperPosition:ws.position,
        wrapperZ:ws.zIndex,overlayZ:os.zIndex,parent:overlay.parentElement.id || overlay.parentElement.tagName,
        overflow:[ws.overflowX,ws.overflowY],viewport:[innerWidth,innerHeight]};
    }, y);
    const expectedTop = origin[1] - (owner === 'ancestor' ? y : 0);
    const near = (a,b) => Math.abs(a-b) <= 0.01;
    requireFact(observed.scrollX === 0 && observed.scrollY === y, ${c.id} actual scroll);
    requireFact(observed.viewport[0] === 900 && observed.viewport[1] === 500, ${c.id} viewport);
    requireFact(observed.position === 'fixed' && observed.insetTop === '0px' && observed.insetLeft === '0px', ${c.id} fixed declarations);
    requireFact(near(observed.left,origin[0]) && near(observed.top,expectedTop), ${c.id} ${phase} geometry at ${y});
    requireFact(near(observed.width,100) && near(observed.height,36), ${c.id} dimensions);
    requireFact(observed.overflow.every(x=>x==='visible'), ${c.id} unexpected scroll container);
    const removed = phase === 'after' && c.action === 'remove';
    const transformed = Boolean(c.style.transform) && !removed;
    requireFact((observed.transform !== 'none') === transformed, ${c.id} computed transform);
    requireFact(observed.willChange === (c.style.willChange || 'auto'), ${c.id} will-change);
    requireFact(observed.overlayZ === (c.overlayZ || 'auto'), ${c.id} overlay z-index);
    const expectedParent = phase === 'after' && c.action === 'reparent' ? 'BODY' : 'wrap';
    requireFact(observed.parent === expectedParent, ${c.id} DOM parent);
    samples.push({phase,expected:{left:origin[0],top:expectedTop,owner},observed});
  }
  return samples;
}

async function main() {
  requireFact(process.argv.length === 3, 'supply one owned Chromium executable path');
  const browser = await chromium.launch({executablePath:process.argv[2],headless:true});
  const context = await browser.newContext({viewport:{width:900,height:500},deviceScaleFactor:1});
  let pageRequests = 0;
  await context.route('**/*', async route => { pageRequests++; await route.abort(); });
  const rows = [];
  try {
    for (const c of CASES) {
      const page = await context.newPage();
      await page.setContent(HTML);
      await page.evaluate(c => {
        Object.assign(document.querySelector('#wrap').style,c.style);
        if (c.overlayZ) document.querySelector('#overlay').style.zIndex=c.overlayZ;
      },c);
      let samples = [];
      if (c.action) {
        samples.push(...await measure(page,c,c.before,'ancestor','before'));
        await page.evaluate(action => {
          if (action === 'remove') document.querySelector('#wrap').style.transform='none';
          if (action === 'reparent') document.body.append(document.querySelector('#overlay'));
        },c.action);
      }
      samples.push(...await measure(page,c,c.final,c.fixedTo,c.action ? 'after' : 'baseline'));
      rows.push({id:c.id,action:c.action || 'none',samples});
      await page.close();
    }
    requireFact(rows.length === 9 && rows.reduce((n,r)=>n+r.samples.length,0) === 36,'case inventory');
    requireFact(pageRequests === 0,'unexpected page resource request');
    const result = {status:'PASS_9_CASES',measurements:36,checked_at:new Date().toISOString(),
      node:process.version,playwright:require('playwright/package.json').version,browser:browser.version(),
      platform:process.platform,arch:process.arch,
      source_sha256:crypto.createHash('sha256').update(fs.readFileSync(__filename)).digest('hex'),
      page_requests:pageRequests,cases:rows,
      scope:'One Chromium build, ordinary desktop layout; no anchor positioning, sticky, modal focus, mobile viewport or framework qualification.'};
    process.stdout.write(JSON.stringify(result,null,2)+'\n');
  } finally {
    await context.close();
    await browser.close();
  }
}
main().catch(error=>{process.stderr.write(String(error)+'\n');process.exitCode=1;});

Run it with one explicitly owned Chromium executable path:

node css_fixed_fixture.cjs /absolute/path/to/chrome > css_fixed_results.json

The preparation run selected Google Chrome for Testing 144.0.7559.96 from Google's official Chrome for Testing distribution. That browser selection is an evidence field separate from Playwright 1.62.1; it is not a claim about what browser Playwright normally bundles. Any new run should report the versions emitted by the script rather than copying the preparation labels.

The fixture creates one browser context with a 900×500 viewport and device scale factor 1, but a fresh page for every case. page.setContent() loads only the embedded HTML. A context route aborts page resource requests and increments a counter, while the final assertion requires that count to remain zero. This keeps the page under test offline and makes unexpected network dependency a HOLD-worthy failure rather than silent test input.

Each measurement performs window.scrollTo(0, target), waits for two requestAnimationFrame callbacks, then reads fresh computed styles and geometry. Those two frame boundaries are the local settling procedure, not a general scheduling guarantee. The test verifies scrollY actually equals the requested target instead of assuming the scroll completed. That distinction is supported by the 8 October 2026 CSSOM View Module Level 1, which defines scrollY as the viewport's y-coordinate relative to the initial containing block origin.

The nine case IDs are preserved because each is a causal control: C01 neutral baseline; C02 relative positioning plus wrapper z-index; C03 identity transform; C04 actual vertical translation; C05 high overlay z-index; C06 will-change: transform with computed transform none; C07 transform removal; C08 transform removal with the hint retained; and C09 reparenting to neutral body while leaving the old wrapper transformed. C07, C08 and C09 each have before and after phases, producing six samples apiece; the other six cases have three samples apiece. Total: nine cases and 36 measured states.

The compact output vocabulary is enough to reconstruct the decision. expected records the literal oracle: left, top and owner. observed records actual scrollX/scrollY, overlay client rectangle, wrapper diagnostic rectangle, overlay position and insets, wrapper transform and willChange, wrapper position and z-index, overlay z-index, DOM parent, overflow values and viewport dimensions. The result object also records status, measurement count, timestamp, Node version, Playwright version, browser version, platform, architecture, source hash, page-request count and scope.

PASS_9_CASES therefore means all 36 observations matched their case-specific expectations and invariants. It does not mean every candidate represents a product-success state. C03, C05, C06 and C08 intentionally validate ancestor-bound geometry that fails the viewport-anchoring product contract. The test suite passes precisely because those negative controls keep their expected mismatch instead of being weakened to make a preferred implementation appear correct.

Observe the identity transform that changes the reference

C01 and C03 isolate how a transformed ancestor affects fixed positioning without changing the overlay declarations. In C01 the wrapper has no transform and the overlay remains at left 0, top 0 / 0 / 0 as document scroll goes 0 / 80 / 200. In C03 the only relevant change is transform: translateX(0px) on the wrapper. The overlay becomes left 60, top 160 / 80 / -40.

That zero-distance transform is visually an identity operation, but the specification condition is not “does this visibly move the ancestor?” It is whether the computed transform is none or not. CSS Transforms explicitly says any computed value other than none affects containing-block and stacking-context creation. translateX(0px) therefore has a structural side effect even though the authored horizontal displacement is zero.

A passing C03 is not evidence of a browser defect. It is evidence that the overlay's measured reference rectangle no longer satisfies the declared viewport contract under that wrapper styling.

Compare none with a zero-distance transform

The control should remain exact: C01 leaves transform absent, so computed transform is none; C03 assigns translateX(0px) and asserts the computed transform is non-none. The harness intentionally does not depend on a particular serialized matrix string because serialization is not the causal question. The binary property relevant to this fixture is none versus non-none.

That is why “but the transform is zero” is not a sufficient repair argument. Identity of visible displacement does not imply identity of layout side effects.

Measure an actual translation without changing the oracle

C04 keeps the same fixed overlay and same wrapper dimensions but uses transform: translateY(20px). The independently authored ancestor origin is therefore 60 pixels from the left and 180 pixels from the top before document scroll. The expected overlay rectangle is left = 60, width = 100, height = 36, with top 180 / 100 / -20 at actual scrollY 0 / 80 / 200.

This case matters because the transform influences both ownership and the rendered client rectangle. CSS Transforms states that transforms affect the client rectangles returned by getClientRects() and getBoundingClientRect(), while CSSOM View defines getBoundingClientRect() as a fresh bounding DOMRect derived from client rects and notes that the returned object is not live.

The fixture therefore reads geometry after every controlled scroll state rather than holding one DOMRect and assuming it updates. Negative top values are not special failures: -20 simply records that the ancestor-bound box has moved 20 CSS pixels above the viewport after 200 pixels of document scroll.

Do not generalize C04 into a transform-matrix tutorial. It proves one authored 20-pixel translation in one flat desktop layout, not arbitrary 3D transform composition or nested transformed coordinate systems.

Inspect will-change even when transform is none

C06 is the control that catches a common incomplete diagnosis. The wrapper has will-change: transform, but no transform declaration is applied, so computed transform is none. The overlay still measures left 60, top 160 / 80 / -40 and therefore follows the ancestor.

The 29 April 2022 CSS Will Change Module Level 1 explains why. If any non-initial value of a property would make an element generate a containing block for fixed-position descendants, naming that property in will-change must make the element generate such a containing block. The draft separately defines analogous stacking-context effects. In other words, will-change: transform is not layout-inert merely because transform itself currently computes to none.

That rule is enough for this decision. The article does not claim GPU promotion, layer creation or a measurable performance benefit, because none of those are tested. The declaration may have been introduced as a rendering hint, but its specified containing-block effect belongs in layout review.

Record the hint as part of the current state

A useful regression record must therefore include both computed transform and computed will-change. Looking only at the declaration that a patch removed can hide the remaining state that still establishes ownership.

C08 makes that concrete later: removing transform while retaining will-change: transform does not restore the viewport baseline. The operational rule is to inventory current relevant properties, not to narrate only the edit you intended to make.

Reject z-index as a repair for this coordinate mismatch

C05 repeats C03's identity-transformed wrapper and changes the overlay's own z-index to 2147483647. If z-index repaired the coordinate system, the measured left/top values would return to the viewport baseline. They do not: C05 remains left 60, top 160 / 80 / -40, matching C03's ancestor-bound geometry.

That is enough to reject “raise the z-index” as a repair for this specific reference-rectangle mismatch. z-index participates in painting order within stacking-context rules; it does not remove the transformed ancestor from the fixed-position containing-block chain. CSS Transforms itself places the stacking-context and containing-block side effects in adjacent but distinct rules, which is exactly why both need separate controls.

C02 blocks the reverse simplification. Its wrapper is position: relative; z-index: 1, yet the overlay stays at 0 / 0 / 0 relative to the viewport. So the evidence does not support “any stacking context traps fixed descendants.” It supports a much narrower statement: the transformed and will-change: transform ancestors exercised here establish the fixed containing block; the C02 relative/z-index state does not reproduce that geometry.

C05 also says nothing about whether the overlay paints above every sibling, remains unobscured, receives pointer input or is usable. Those are different acceptance questions.

Reconcile the requested scroll with each measured rectangle

The preparation ledger below keeps the causal state, observed coordinates and interpretation together. Actual scrollY is asserted equal to each requested offset before geometry is accepted, so a clamped or failed scroll cannot masquerade as positioning behavior.

Case

Controlled state or change

Left

Top at actual document scroll 0 / 80 / 200

Interpretation

C01

Neutral wrapper

0

0 / 0 / 0

Viewport baseline

C02

Wrapper position:relative; z-index:1

0

0 / 0 / 0

Positioned stacking context alone does not cause this fixed geometry

C03

Wrapper translateX(0px)

60

160 / 80 / -40

Identity transform changes the reference rectangle

C04

Wrapper translateY(20px)

60

180 / 100 / -20

Ancestor translation is included in client geometry

C05

C03 plus overlay z-index:2147483647

60

160 / 80 / -40

Higher paint-order value does not restore viewport coordinates

C06

will-change:transform, computed transform none

60

160 / 80 / -40

The hint establishes ancestor ownership here

C07

Remove transform, no will-change

60 → 0

160 / 80 / -40 → 0 / 0 / 0

Measured style repair restores viewport baseline

C08

Remove transform, retain will-change:transform

60

160 / 80 / -40 before and after

Proposed style change is insufficient

C09

Move same overlay to neutral body

60 → 0

160 / 80 / -40 → 0 / 0 / 0

Measured DOM relocation restores viewport baseline

The harness checks more than those four coordinate columns. Every sample must keep the overlay at position: fixed, with computed top: 0px and left: 0px, dimensions 100×36, a 900×500 viewport, and wrapper overflow visible/visible. It also checks transform presence, will-change, overlay z-index and the expected DOM parent. Those state fields prevent the team from attributing a coordinate change to the wrong edit.

The distinction between diagnostic pass and product acceptance is crucial. PASS_9_CASES says the prepared observations matched the declared expectations for all controls, including the intentionally failing candidates. For a product whose requirement is viewport anchoring, C01 and C02 satisfy geometry directly; C07-after and C09-after are measured successful repair states. C03, C04, C05, C06 and C08 remain useful because they preserve the failure signatures the repair must eliminate.

That is also why the ledger keeps actual scroll beside geometry. A top value of 80 has no diagnostic meaning by itself; it becomes evidence only when paired with scrollY = 80, the ancestor origin of 160, and the current transform or will-change state. Reviewers should be able to recompute the expected top from authored constants without consulting the observed wrapper rectangle. If they cannot, the test has drifted from an independent oracle into a self-referential measurement.

Retain a complete state record for every phase

C07, C08 and C09 each record three before samples and three after samples. A final screenshot cannot establish causality. If only C07-after were retained, a reviewer could see {0,0} without knowing whether transform was actually removed, whether scroll occurred, or whether some unrelated DOM move changed the result.

The same principle applies to DOMRects. CSSOM View says the rectangle object returned by getBoundingClientRect() is not live, so the fixture obtains a fresh snapshot after each scroll and each controlled mutation. The evidence record should preserve both sides of a repair, not a stale rectangle captured before the state changed.

Remove a transform only when its ownership is understood

C07 is the measured style repair. It begins with the same identity-transformed wrapper as C03, so before removal the overlay is ancestor-bound at left 60, top 160 / 80 / -40. The action assigns transform: none. With no will-change declaration in that case, the after phase returns to left 0, top 0 / 0 / 0. For this fixture and recorded Chrome build, that is a successful viewport-anchoring repair.

C08 proves why “remove the transform” can be incomplete. Its initial wrapper has both transform: translateX(0px) and will-change: transform. The action removes only transform. Computed transform becomes none, but the hint remains; before and after coordinates are both left 60, top 160 / 80 / -40. That result aligns with the Will Change containing-block rule.

A production repair still needs ownership review. The ancestor style may belong to a transition, motion utility, layout component or other contract outside the overlay. Removing it merely because the overlay benefits can break an unrelated owner. The fixture measures geometry only; it does not measure animation quality, rendering performance or component semantics.

Removing both transform and a persistent will-change: transform may be a reasonable separately reviewed change, but it is not a tenth executed case. Only C07 demonstrates successful style removal in the prepared evidence. Keep that wording precise.

Evaluate DOM relocation against a known-neutral host

C09 tests the second measured repair path: change DOM ownership instead of ancestor styling. The before phase is the same ancestor-bound identity-transform state, at left 60, top 160 / 80 / -40. The action executes document.body.append(document.querySelector('#overlay')), leaving the wrapper transformed. After the overlay is no longer a descendant of that wrapper, its measured rectangle becomes left 0, top 0 / 0 / 0.

That result is causal in this deliberately neutral document because the new ancestor chain contains no tested fixed-containing-block creator. The old transformed wrapper can no longer establish the overlay's containing block once it is not an ancestor.

Do not turn that controlled observation into “move fixed overlays to body.” The production host must be inspected for its own relevant styles. This fixture expressly excludes root/body transform variants, containment/filter alternatives, framework portals and arbitrary ancestor combinations. Reparenting can also change inheritance, event ownership, styling assumptions and component lifecycle behavior. None of those integration effects are qualified here.

So C09 authorizes a bounded statement: DOM relocation to this neutral body restores the declared viewport geometry in the recorded environment. It does not certify every body host or portal architecture. If the proposed host's containing-block state is unknown, HOLD until it is measured.

Choose a bounded repair and state what remains untested

The decision should be made against the declared reference rectangle, not against a preferred technique.

Situation

Decision

Owner

Evidence required

Overlay must follow viewport and measures {0,0} at all asserted scroll values

ACCEPT_GEOMETRY

Overlay/component owner

Actual scroll, fixed declarations, relevant ancestor state, matching rectangle

Overlay is intentionally ancestor-attached

ACCEPT_GEOMETRY only if that is the product contract

Product/layout owner

Declared ancestor reference plus matching measured geometry

Unexpected transform creates ancestor-bound geometry

REPAIR

Owner of ancestor styling

Causal before/after state and owner approval

Transform removed but will-change:transform remains and geometry is still ancestor-bound

REPAIR or HOLD

Ancestor-style owner

Current computed will-change, expected owner, retest after reviewed change

Relocation to a verified neutral host restores viewport coordinates

REPAIR path validated geometrically

Overlay-host owner

Before/after DOM parent, host style inventory, actual scroll and geometry

Untested engine, creator property, host chain or missing expected owner

HOLD

QA + relevant component owner

Supported-browser run and complete state record

A geometry repair is not full component acceptance. Refonte Learning's separate native-dialog focus lifecycle exists because modal interaction requires evidence about focus ownership and close behavior that a rectangle test cannot provide. This article does not introduce showModal(), top-layer behavior or dialog semantics as remedies for the ordinary fixed div under test.

Separate geometry acceptance from component acceptance

ACCEPT_GEOMETRY means only that the measured overlay rectangle matches its declared reference rectangle in the recorded environment. It does not establish usable visibility, pointer hit testing, keyboard behavior, accessibility, focus restoration, mobile visual-viewport behavior or support in another engine.

Those obligations need independent acceptance criteria. A team should resist upgrading a precise geometry pass into a general “overlay works” claim.

The HOLD state is equally important. It is not a failure label for uncertain engineers; it is the correct operational result when evidence is insufficient to assign ownership. If a supported browser has not been run, if a host's relevant ancestor styles are unknown, or if the product team has never stated whether the overlay should follow the viewport or a component shell, there is no defensible geometry acceptance decision yet. The next action is to gather the missing field, not to extrapolate from the Chrome reference run.

Keep the ancestor contract stable during maintenance

The regression becomes easier to prevent when ownership is explicit. The ancestor-style owner is responsible for knowing whether transform or will-change may alter descendants' fixed containing blocks. The overlay-host owner is responsible for choosing and maintaining the DOM ancestor chain that satisfies the reference-rectangle contract. QA owns the reproducible evidence: runtime versions, source identity, actual scroll, computed ancestor properties, DOM parent and client geometry.

Reopen qualification when any of those inputs changes materially. A new transform, a retained hint, a different host, changed fixture dimensions or a newly supported browser engine can invalidate an old conclusion. Keep the old failing control in the regression suite; do not replace C03 or C06 with only repaired states. Negative controls show that the test is still capable of detecting the mechanism the patch is supposed to prevent.

Likewise, preserve the intended rectangle as authored test data. If reviewers quietly redefine the expected coordinates to match a new observation, the test stops protecting the product contract. The oracle should change only when the product requirement changes deliberately.

A compact maintenance record should therefore travel with the regression: the case ID or application scenario, expected owner, expected rectangle at named scroll offsets, source revision, runtime identifiers and the exact style or DOM change under review. That record makes later triage faster because it separates environment drift from contract drift. A browser-version change may require requalification; a product requirement change may require a new oracle; neither should be disguised as a harmless update to expected numbers.

Rollback should also be concrete. Reverting an ancestor style or host placement is a reviewed implementation action, not an assumption that every side effect is automatically reversible. If the ancestor belongs to another component, coordinate the repair with that owner. If the host changes, inspect integration obligations separately. When those owners or effects are unknown, HOLD rather than treating the laboratory's geometric success as deployment approval.

Develop stronger CSS debugging habits with Refonte Learning

This exercise rewards a transferable frontend habit: turn a visual symptom into an explicit browser contract, isolate one causal property at a time, and preserve before/after evidence. Refonte Learning's broader frontend learning path places HTML/CSS, JavaScript, responsive design and component work in a wider learning sequence; the fixed-containing-block fixture here is a focused engineering exercise rather than a confirmed item in that article's curriculum.

For structured study, the Frontend Development programme page lists HTML5/CSS3, JavaScript ES6+, responsive web design, React, Redux, API/AJAX integration, Git, accessibility/performance topics and a capstone; its FAQ also names Git, npm, React and Visual Studio Code. It lists Potential Internship and, after successful completion, a Training Certificate plus Certificate of Internship.

A useful next step is smaller than adopting any new tool: choose one overlay you maintain, write down the rectangle it is supposed to follow, and keep a measured before/after record that includes actual scroll, ancestor transform, ancestor will-change, DOM parent and getBoundingClientRect(). That habit turns “fixed is moving” from a vague browser complaint into an ownership decision you can review, repair and retest.