A component observes a target with threshold: 0.5, receives an IntersectionObserver callback, and immediately runs an action. The code looks plausible because the threshold sounds like a gate: ask for 50%, get callbacks at 50%, therefore every delivered entry must satisfy 50%. That last inference is the defect.
The operational boundary is narrower: notification answers whether the browser has an observation to deliver; the application must separately decide whether that entry authorizes its action. The Intersection Observer Editor’s Draft dated March 19, 2026 computes a threshold index and queues an entry when relevant observation state changes. It does not define your component’s business action. A downward threshold crossing can therefore produce the very callback that tells you the target is now below the required ratio.
This playbook validates that boundary with one explicit 200×200 root and one nonzero 100×100 target. The preparation record is Chromium 144.0.7559.96, Playwright 1.57.0, CPython 3.13.5, and Linux. There are no borders, padding, transforms, occlusion tests, or nested frames. The deliverable is a ten-case ledger that keeps browser rectangles, an independent rectangular-clipping oracle, intersectionRatio, isIntersecting, configuration, and action eligibility in the same evidence unit. Other browsers remain untested. The point is not to make the observer quieter; it is to make every action explainable from recorded state and reviewed policy.
Define the minimum-area action separately from notification
Start with the product rule, not with the callback signature. In this fixture the rule is deliberately small: the target has nonzero area, and the action may run only when at least half of the target’s bounding-box area lies inside the explicit root and the delivered entry says it is intersecting. The reviewed entry predicate is therefore:
entry.isIntersecting && entry.intersectionRatio >= 0.5That predicate is necessary but not sufficient by itself. The observer configuration must also be approved: the code must have constructed the intended observer and read back the effective threshold list. For the principal cases, the expected list is [0.5]; for the comparison case it is [0, 0.5]. A configuration typo is a separate rejection even if a particular entry happens to have a ratio that would fail the action anyway.
For a nonzero target, the draft computes intersectionRatio as intersection area divided by target area. MDN’s intersectionRatio reference describes the same area relationship; its zero-area special case is deliberately outside this fixture. The 100×100 target has area 10,000 CSS px², so 25%, 50%, and 75% correspond to clipped intersection areas of 2,500, 5,000, and 7,500 CSS px² in this controlled vertical-motion setup.
Do not encode the requirement as “callback happened,” and do not reduce it to entry.isIntersecting. Those are different propositions. The callback is a delivery mechanism. isIntersecting is an entry field with defined geometry semantics in the draft and an implementation-history nuance discussed later. The ratio expresses the minimum-area quantity the component actually cares about. The component owner should be able to point to all three separately: intended configuration, observed entry, and action decision.
This matters even when the callback receives only one entry in a tiny fixture. The callback is allowed to receive an array of entries, and the application may eventually observe more than one target. A callback-level boolean such as shouldActivate = true throws away which target and which geometry justified the decision. Keep qualification inside the per-entry loop so the evidence model scales without changing its meaning.
That separation produces the acceptance rule for this article: ACCEPT only the reviewed configuration plus the explicit entry predicate under the tested geometry. REPAIR callback-only or isIntersecting-only dispatch. RERUN when browser or layout evidence is missing. HOLD broader claims that the fixture did not test.
Freeze the browser, root and target geometry
Geometry-dependent tests become weak when their coordinate system is implicit. This fixture makes the root a 200×200 positioned element at page coordinates (20, 20) because the root has a 20-pixel margin and the body has zero margin. The target is a 100×100 absolutely positioned child whose left remains zero while its top changes between cases. The browser context uses an 800×600 viewport and device scale factor 1. Root and target have no borders or padding, so the simple rectangle edges used by the oracle are the relevant edges for this bounded test.
That is a different ownership question from scroll-container ownership, where a sticky element’s behavior depends on which scroller supplies its positioning reference. Here the root is fixed on purpose and only one target moves. The neighboring article is useful as a reminder that browser geometry has owners, but it does not prove any IntersectionObserver result in this fixture.
The preparation record reports these facts together:
Evidence field | Preparation record |
Batch date | 2026-10-22 |
Python | 3.13.5 (GCC 14.2.0 build shown in the run) |
Playwright | 1.57.0 |
Platform | Linux-6.18.55-x86_64-with-glibc2.41 |
Browser | Chromium 144.0.7559.96 |
Viewport / scale | 800×600 / 1 |
Started UTC | 2026-10-22T06:27:53.945809+00:00 |
Finished UTC | 2026-10-22T06:27:55.433725+00:00 |
Status | PASS_10_BOUNDED_CASES_WITH_DIVERGENCES |
Source SHA-256 | a1fc222e4705b06c61303454bf9281fd0aeda682519bf12b412cf3188af819f9 |
The source digest identifies exact bytes; it is not evidence that those bytes executed. Conversely, Playwright’s package version is not a browser version. The browser version must be read from the launched executable and recorded separately.
A separate verification run used a runnable file reconstructed from the supplied source and executed it in an environment that independently reported the same Python, Playwright, Linux, and Chromium versions. It reproduced the ten ratios, flags, threshold lists, and action decisions. Its reconstructed file hash was a13a2d4a562d0eab70a2589890c4d7b2e64002dc559371ab46ff90e9e6ff3076, not the preparation hash, so that execution is corroborating evidence only; it must not be mislabeled as the exact preparation artifact.
The exclusions matter. With no transforms, nested clips, borders, padding, zero-area target, or cross-document frames, a simple rectangular oracle is appropriate to this fixture. It is not a replacement for the draft’s general “compute the intersection” algorithm. Any production layout that introduces those excluded conditions needs fresh evidence rather than extrapolation.
Build the observation ledger and rectangular oracle
The fixture records every delivered entry before any real side effect exists. Each row includes the stage label, browser intersectionRatio, browser isIntersecting, the target, root, and intersection rectangles, the observer’s effective thresholds, a deliberately bad callback-only action (naiveAction: true), an isIntersecting-only candidate, and the reviewed predicate. Python then calculates an independent ratio from the serialized rectangles and asserts that it matches the browser ratio for this bounded geometry.
The complete fixture is below. The preparation SHA-256 above belongs to the original preparation file, not to the copy shown here.
from future import annotations
import hashlib, importlib.metadata, json, os, platform, sys
from datetime import datetime, timezone
from pathlib import Path
from playwright.sync_api import sync_playwright
HTML = '''<!doctype html><meta charset="utf-8"><title>Intersection qualification lab</title>
<style>
body { margin:0; }
#root { position:relative; margin:20px; width:200px; height:200px; overflow:hidden; }
#target { position:absolute; left:0; top:230px; width:100px; height:100px; }
</style><div id="root"><div id="target">fixture</div></div>'''
SCRIPT = r'''() => {
const root = document.getElementById('root');
const target = document.getElementById('target');
let observer = null;
let waiting = null;
let stage = '';
window.records = [];
const rect = r => ({x:r.x,y:r.y,width:r.width,height:r.height,
top:r.top,right:r.right,bottom:r.bottom,left:r.left});
function receive(entries) {
for (const entry of entries) {
const row = {stage, ratio:entry.intersectionRatio,
isIntersecting:entry.isIntersecting, time:entry.time,
target:rect(entry.boundingClientRect), root:rect(entry.rootBounds),
intersection:rect(entry.intersectionRect),
thresholds:Array.from(observer.thresholds),
naiveAction:true, intersectOnly:entry.isIntersecting,
qualified:entry.isIntersecting && entry.intersectionRatio >= 0.5};
window.records.push(row);
if (waiting) { const done=waiting; waiting=null; done(row); }
}
}
window.sample = (label, top, restart, mode) => new Promise((resolve,reject) => {
if (waiting) { reject(new Error('Previous sample is still pending')); return; }
stage=label;
const timer=setTimeout(()=>{waiting=null;reject(new Error('No observation: '+label));},3000);
waiting=row=>{clearTimeout(timer);resolve(row);};
if (restart && observer) observer.disconnect();
target.style.top=top+'px';
if (restart) {
const options=mode==='typo' ? {root,thresholds:[0.5]} :
{root,threshold:mode==='zero_and_half' ? [0,0.5] : 0.5};
observer=new IntersectionObserver(receive,options);
observer.observe(target);
}
});
window.finish=()=>observer.disconnect();
}'''
def main():
start=datetime.now(timezone.utc).isoformat()
scenarios=[('I1',230,True,'half',0.),
('I2',175,True,'half',0.25),('I3',125,False,'half',0.75),
('I4',175,False,'half',0.25),('I5',230,True,'half',0.),
('I6',200,True,'half',0.),('I7',150,False,'half',0.5),
('I8',175,False,'half',0.25),('I9',175,True,'zero_and_half',0.25),
('I10',175,True,'typo',0.25)]
with sync_playwright() as p:
executable=os.environ.get('CHROMIUM_EXECUTABLE','/usr/bin/chromium')
browser=p.chromium.launch(executable_path=executable,headless=True,args=['--no-sandbox'])
try:
context=browser.new_context(viewport={'width':800,'height':600},device_scale_factor=1)
page=context.new_page();page.set_content(HTML);page.evaluate(SCRIPT)
rows=[]
for case,top,restart,mode,ratio in scenarios:
row=page.evaluate('(a)=>window.sample(...a)',[case,top,restart,mode])
assert abs(row['ratio']-ratio)<1e-7, row
assert row['thresholds']==(
[0] if mode=='typo' else
[0,0.5] if mode=='zero_and_half' else
[0.5]
), row
# Independent rectangular clipping oracle for this no-border fixture only.
t,r=row['target'],row['root']
width=max(0.,min(t['right'],r['right'])-max(t['left'],r['left']))
height=max(0.,min(t['bottom'],r['bottom'])-max(t['top'],r['top']))
oracle=width*height/(t['width']*t['height'])
assert abs(oracle-row['ratio'])<1e-7
assert row['qualified'] is (case in {'I3','I7'})
row['oracle_ratio']=oracle
row['configuration_approved']=(mode!='typo')
normative=(t['right']>=r['left'] and t['left']<=r['right'] and
t['bottom']>=r['top'] and t['top']<=r['bottom'])
row['spec_geometric_isIntersecting']=normative
row['flag_differs_from_spec_geometry']=(
row['isIntersecting']!=normative
)
rows.append(row)
page.evaluate('window.finish()')
manifest=dict(
browser=browser.version,
user_agent=page.evaluate('navigator.userAgent'),
viewport={'width':800,'height':600},
device_scale_factor=1
)
context.close()
finally:
browser.close()
print(json.dumps(dict(
status='PASS_10_BOUNDED_CASES_WITH_DIVERGENCES',
started=start,
finished=datetime.now(timezone.utc).isoformat(),
python=sys.version,
playwright=importlib.metadata.version('playwright'),
platform=platform.platform(),
source_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
**manifest,
cases=rows
),indent=2))
if name=='__main__':
main()Run it with the intended local executable, setting CHROMIUM_EXECUTABLE when /usr/bin/chromium is not the browser under review, then invoke python intersection_lab.py. A different executable version is a different test record. The three-second timer is only a failure bound for a missing expected sample; it does not establish an IntersectionObserver delivery-time contract.
A useful evidence unit is therefore larger than a screenshot or a console line. For each case, retain the input position, whether the observer was restarted, the construction mode, effective threshold list, all three rectangles, browser fields, oracle fields, expected action, and assertion outcome. When a browser revision changes one field, that structure shows exactly what moved while leaving unrelated facts intact.
Measure area rather than interpreting the callback name
For each row, the oracle clips the target to the root independently of intersectionRatio: intersection width is max(0, min(right edges) - max(left edges)), and height is the corresponding vertical expression. The oracle ratio is (width × height) / (target width × target height). With a nonzero 100×100 target this is well-defined.
This is deliberately more useful than asserting “a callback fired.” A callback occurrence contains no quantity. The serialized rectangles let a reviewer recompute the area. The ratio lets the component compare that area against its 0.5 policy. The action boolean lets the test prove that the side effect would be admitted only for the intended rows. A successful assertion can therefore prove that a candidate rule is wrong (for example, naiveAction: true is demonstrably over-permissive) without approving that candidate.
Prove that the initial callback is not an eligibility signal
I1 starts a new observer while the target is fully outside the root. The delivered ratio is 0 and isIntersecting is false. If the component runs its action merely because the callback ran, I1 is already a decisive counterexample: an initial observation was delivered, yet the minimum-area requirement was not met.
I2 makes the point stronger. A new observer begins with the target at 25% overlap. The browser reports intersectionRatio = 0.25, below the configured 0.5 threshold. In the preparation Chromium build it reports isIntersecting = false as well; that flag is part of the implementation discrepancy examined later. Regardless of that flag, the area rule says no action: 2,500 of 10,000 CSS px² is only one quarter of the target.
MDN’s observe() reference, last modified June 16, 2026, explicitly documents that observing a target causes an initial callback on the first rendering cycle even when the target has not moved; its outside-target example produces an initial non-intersecting entry. The March 19, 2026 editor’s draft models newly observed targets with initial registration state and separately defines “pending initial IntersectionObserver targets” as targets for which no entry has yet been queued. Neither source turns initial delivery into a promise that your application threshold is satisfied.
An observation can be required even when no action qualifies
This distinction is operationally valuable. Observation setup asks the browser to establish state; action qualification asks whether a particular state meets the component’s contract. Initial delivery can be necessary for the observation system even when the action ledger records false.
That is also why observer teardown is not the main diagnosis here. Listener lifecycle ownership concerns whether delivery continues after an owner is disposed. This fixture disconnects its observer as cleanup, but it is testing a different boundary: whether a validly delivered entry authorizes an action.
Treat I1 and I2 as mandatory negative cases in regression coverage. If a refactor makes either action boolean true, the suite should fail even though callback delivery itself remains healthy. Negative evidence protects the policy boundary better than a single happy-path “50% caused an action” assertion. It also prevents a common test smell: initializing the target directly in the passing state and concluding that the observer “works.” A passing initial state can prove one eligible record; it cannot expose whether an ineligible initial record would wrongly dispatch the same action.
Follow upward and downward threshold crossings
The next failure mode appears after the initial state. I2, I3, and I4 use the same observer after I2. The target begins at 25%, moves to 75%, then falls back to 25%. I3 is eligible: intersectionRatio = 0.75, the preparation Chromium flag is true, configuration is [0.5], and the reviewed predicate evaluates true. I4 is not eligible: the ratio falls to 0.25 and the predicate becomes false.
Why does I4 have a callback at all if the threshold is 0.5? Because a threshold is a state boundary, not a one-way permission token. The editor’s draft computes thresholdIndex from the current ratio: with [0.5], 0.75 is at or above the last threshold, while 0.25 is below it. The draft queues an entry when the threshold index differs from the previous threshold index, among other state-change conditions. Moving upward across 0.5 changes the index; moving downward across 0.5 changes it again.
I7 and I8 repeat the same reasoning around the exact boundary. I7 moves from an edge-adjacent initial state to exactly 50%; Chromium reports ratio 0.5 and isIntersecting = true, so the inclusive policy >= 0.5 accepts it. I8 then falls to 25%, producing another delivered record that must be rejected by the action gate.
The important phrase for maintainers is IntersectionObserver downward threshold crossing. It should appear in test names and review comments because it is the concrete case that callback-only code forgets. A callback can describe entering the acceptable region or leaving it. The observer reports a change; your component decides what the new state means.
The falling edge belongs in the action test
The smallest broken implementation is:
const observer = new IntersectionObserver(() => {
activateComponent();
}, { root, threshold: 0.5 });It has no evidence gate at all. I4 and I8 both make it run after the ratio has fallen to 0.25. Replacing the empty parameter list with entries is not enough unless the code evaluates each entry. A batch may contain multiple entries, and action eligibility belongs to each entry’s state, not to the fact that the callback was invoked.
The repair is therefore not “ignore the second callback.” That would hard-code an accidental sequence. Keep receiving records, including falling records, and apply the policy to each one. This makes the component correct whether an entry is initial, upward-crossing, or downward-crossing under the tested contract.
Keep the specification and Chromium observation in separate columns
There is a real nuance in the preparation record, and hiding it would make the playbook less reliable. The editor’s draft says isIntersecting is true when targetRect and rootBounds intersect or are edge-adjacent, even when the intersection has zero area. It then computes the ratio and threshold index separately. Under that geometry rule, I2, I4, I6, and I8 are intersecting: I2/I4/I8 have positive 25% overlap, and I6 is exactly edge-adjacent. Yet Chromium 144.0.7559.96 reported isIntersecting = false for those four rows when the single configured threshold was 0.5.
This is not a reason to rewrite the draft in the article, and it is not evidence that every engine behaves like this Chromium build. The W3C IntersectionObserver issue #432, opened May 23, 2020, documents the historical mismatch between the specification’s isIntersecting definition and browser behavior when nonzero thresholds are involved. The discussion specifically describes Chromium behavior that could set isIntersecting false below the lowest nonzero threshold and contrasts that with the geometric definition. The related Chromium issue 40693914 is the implementation report identified for this class of discrepancy. It is cited as a historical reference; no additional present-day Chromium contract is inferred from its access-limited contents.
The preparation investigation also matters. An earlier draft of the lab expected an already-observed target to move from fully outside to 25% overlap under threshold: 0.5 and notify because geometric isIntersecting should change under the editor’s draft algorithm. In the tested Chromium build that wait timed out. The final ten-case lab does not pretend that transition was observed. Instead, it creates a new observer to capture the initial 25% case I2, and another new observer for edge-adjacent I6. That preserves what was actually measured.
Maintain two columns in the ledger: Observed isIntersecting and Geometric isIntersecting. The first is browser evidence. The second is the bounded oracle derived from the draft’s intersection-or-edge-adjacent definition and the serialized rectangles. A discrepancy flag is data, not a test failure to suppress.
That is why the status string PASS_10_BOUNDED_CASES_WITH_DIVERGENCES must be read literally. The ratio assertions, threshold-list assertions, oracle comparisons, and action decisions passed while known flag divergences were retained. It does not say “Chromium passed a conformance test.” Conformance is a broader question and this fixture is not a WPT replacement.
For code review, this separation also changes the language of bug reports. “The specification says Chromium is wrong” is too coarse for an application repair ticket, just as “Chromium says false, therefore the draft means false” is incorrect. Record the draft-derived geometry expectation, the browser-produced flag, and the component decision independently. The component can remain safe even while those first two columns disagree because its acceptance predicate is evaluated explicitly.
Test edge contact and the zero-threshold comparison
I6 places the target at top: 200px within a root whose content geometry ends at local y=200. In page coordinates, the root bottom and target top are both y=220. The intersection rectangle has width 100 and height 0, so its area and ratio are zero. Under the editor’s draft’s geometric rule, the rectangles are edge-adjacent and therefore isIntersecting is true. The tested Chromium build instead reports false with a single 0.5 threshold.
The action outcome is uncomplicated: zero area cannot satisfy a 50% minimum. Even an engine that reports the draft-geometric isIntersecting = true at edge contact should still produce qualified = false because the ratio is 0. This is why “IntersectionObserver edge adjacent” belongs in a policy test: it separates intersection-state semantics from the minimum-area action requirement.
I9 then creates a new observer at 25% overlap with threshold: [0, 0.5]. In the preparation Chromium build, isIntersecting is true and the ratio is 0.25. The configuration is intentional and is read back as [0, 0.5]; the action still remains false. Adding zero to the threshold list changed the observed flag behavior in this comparison, but it did not change the business minimum from 50%.
isIntersecting alone is not the business ratio policy
I9 is the direct counterexample to an isIntersecting-only action gate in the same browser used for the main preparation evidence:
if (entry.isIntersecting) {
activateComponent(); // Wrong for I9: ratio is only 0.25.
}The repair is not to add zero to every threshold array. That would confuse a comparison case with a universal workaround and would still leave isIntersecting insufficient for a 50% policy. The repair is to express the ratio policy directly.
This separation resembles another frontend boundary, DOM structure versus interaction contracts, where looking structurally correct does not establish the interaction behavior a component requires. The linked article tests cloning and listener bindings; it does not supply evidence for IntersectionObserver. Here, similarly, one boolean field cannot stand in for a different quantitative contract.
Use I6 and I9 together. I6 shows that geometric contact can have zero area. I9 shows that positive intersection can still be below the application minimum. Between them, “intersecting” and “eligible” are forced apart in review.
Read back the actual threshold list
Configuration deserves its own acceptance check because JavaScript makes a particularly easy typo available. The constructor option is singular: threshold. The observer’s read-only property is plural: thresholds. The editor’s draft defines the IntersectionObserverInit member as threshold, sorts the effective list, defaults it to zero when omitted or empty, and returns the resulting list through the thresholds getter. MDN’s thresholds reference calls out the exact trap: using thresholds in the constructor options does not configure the intended threshold, so the effective list becomes [0].
I10 deliberately makes that mistake:
new IntersectionObserver(receive, { root, thresholds: [0.5] });At 25% overlap, the preparation Chromium build reports ratio 0.25, isIntersecting = true, and, most importantly, observer.thresholds equals [0]. The action ratio predicate is false anyway, but that does not rescue the configuration. The fixture marks configuration_approved = false.
This is an important test-design pattern: do not let a negative action result hide a configuration failure. Suppose a regression suite only asserted that I10 did not activate. It would pass, but for the wrong reason. Later, a 60% entry could activate under an observer that was configured differently from what the component owner intended. The correct evidence records both “did this entry qualify?” and “did we actually build the observer we reviewed?”
For a component whose required list is [0.5], make the readback explicit:
function hasExpectedThresholds(observer) {
return observer.thresholds.length === 1 &&
observer.thresholds[0] === 0.5;
}For a component intentionally using [0, 0.5], the expected list is different. Do not write one global validator that rejects every threshold array other than [0.5]; configuration approval belongs to the component’s declared policy.
Also resist a common documentation shortcut: “we passed threshold: 0.5, therefore configuration is proven.” Input text is not runtime evidence. Reading observer.thresholds catches misspellings, empty/default behavior, and sorting behavior at the API boundary. The app can fail fast in development or the integration test can reject the run before action assertions are interpreted.
When multiple thresholds are intentional, compare the whole normalized list rather than checking only that 0.5 appears somewhere. The draft sorts the list, so the readback is also a canonical representation of what the observer retained. Configuration review should document whether extra thresholds are part of the contract, because an additional threshold can legitimately create additional notification opportunities even though it does not itself authorize an action.
Implement the reviewed entry predicate
Production code should make the notification/action split visible enough that a reviewer can audit it without reconstructing intent from a callback body. One compact pattern is to validate configuration once, then qualify each delivered entry:
const MIN_RATIO = 0.5;
let configurationApproved = false;
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
const eligible = configurationApproved &&
entry.isIntersecting &&
entry.intersectionRatio >= MIN_RATIO;
actionLedger.push({
ratio: entry.intersectionRatio,
isIntersecting: entry.isIntersecting,
eligible,
});
if (eligible) activate(entry.target);
}
}, { root, threshold: MIN_RATIO });
configurationApproved =
observer.thresholds.length === 1 &&
observer.thresholds[0] === MIN_RATIO;
if (!configurationApproved) {
throw new Error('IntersectionObserver threshold configuration rejected');
}
observer.observe(target);In the lab, replace activate() with an inert boolean or ledger append. Destructive or costly side effects do not belong in a diagnostic fixture. The owner of the component’s action policy should approve the MIN_RATIO and any isIntersecting requirement; the frontend platform or integration-test owner should approve the runtime configuration and evidence capture. Those responsibilities can live in the same team, but they answer different questions.
For this nonzero rectangular target, ratio >= 0.5 already implies positive intersection area in ordinary geometry. Keeping the explicit isIntersecting conjunction is still the reviewed policy for this playbook: it refuses action if the browser delivers internally inconsistent fields, and it makes the component’s use of both entry semantics visible. That is a conservative application choice, not a claim that the draft mathematically requires both checks for a positive-area threshold.
Separate callback delivery from action dispatch in the code
The action ledger should contain one decision per entry, not one decision per callback. With the ten prepared rows, only I3 and I7 reach the action path. I1, I2, I4, I5, I6, I8, and I9 fail the entry predicate; I10 also fails configuration approval.
That distinction improves failure messages. “Callback did not run” is a delivery failure. “Ratio was 0.25” is an eligibility rejection. “Effective thresholds were [0]” is a configuration rejection. Combining all three into “lazy activation failed” throws away the very evidence needed to choose the repair owner.
Reconcile all ten records against geometry and expected decisions
The ledger below is the acceptance evidence. Rectangles are serialized as (x, y, width, height) in CSS pixels. The root is constant at (20,20,200,200). Geom. is the independently calculated geometric isIntersecting value for this bounded fixture; Oracle is the independently calculated area ratio. Action means the reviewed entry predicate and configuration admit the action.
Case | Thresholds | Target rect | Root rect | Intersection rect | Browser ratio | Oracle | Observed isIntersecting | Geom. | Config | Action |
I1 | [0.5] | (20,250,100,100) | (20,20,200,200) | (0,0,0,0) | 0.00 | 0.00 | false | false | approve | no |
I2 | [0.5] | (20,195,100,100) | (20,20,200,200) | (20,195,100,25) | 0.25 | 0.25 | false | true | approve | no |
I3 | [0.5] | (20,145,100,100) | (20,20,200,200) | (20,145,100,75) | 0.75 | 0.75 | true | true | approve | yes |
I4 | [0.5] | (20,195,100,100) | (20,20,200,200) | (20,195,100,25) | 0.25 | 0.25 | false | true | approve | no |
I5 | [0.5] | (20,250,100,100) | (20,20,200,200) | (0,0,0,0) | 0.00 | 0.00 | false | false | approve | no |
I6 | [0.5] | (20,220,100,100) | (20,20,200,200) | (20,220,100,0) | 0.00 | 0.00 | false | true | approve | no |
I7 | [0.5] | (20,170,100,100) | (20,20,200,200) | (20,170,100,50) | 0.50 | 0.50 | true | true | approve | yes |
I8 | [0.5] | (20,195,100,100) | (20,20,200,200) | (20,195,100,25) | 0.25 | 0.25 | false | true | approve | no |
I9 | [0,0.5] | (20,195,100,100) | (20,20,200,200) | (20,195,100,25) | 0.25 | 0.25 | true | true | approve | no |
I10 | [0] | (20,195,100,100) | (20,20,200,200) | (20,195,100,25) | 0.25 | 0.25 | true | true | reject | no |
Four rows (I2, I4, I6, and I8) preserve the observed Chromium/draft-geometry difference rather than smoothing it away. That makes the ledger more useful than a single green test status. I9 proves in the same browser that isIntersecting = true does not imply the application’s 50% requirement. I10 proves that a non-action can coincide with bad configuration. I3 and I7 are the only accepted actions because they meet the reviewed predicate under approved configuration.
Read the duplicated 25% rows deliberately rather than treating them as redundant. I2 is an initial observation with a single nonzero threshold; I4 and I8 are falling records after previously eligible states; I9 is an initial observation under [0, 0.5]; I10 is an initial observation under accidental [0]. Their geometry can match while their history or configuration differs. That is exactly why the case identifier, restart mode, and threshold list belong beside the rectangles: equal ratios do not imply equal diagnostic meaning.
The ledger also keeps native browser geometry boundaries conceptually separate from this test. Anchor positioning is another geometry-sensitive browser feature, but support or behavior claims from that article do not establish IntersectionObserver parity, and this one-browser ledger must not borrow them.
A good regression test should assert the negative rows as aggressively as the positive rows. Specifically, I4 and I8 catch callback-only dispatch after a downward crossing; I9 catches isIntersecting-only dispatch; I10 catches option-name/configuration drift; I6 keeps zero-area edge contact visible; and I1/I2 keep initial delivery from being mistaken for permission.
Bound what this test says about visibility
IntersectionObserver geometry is often described with the everyday word “visibility,” but that word carries more product meaning than this fixture measures. The editor’s draft’s core algorithm computes target, root, intersection geometry, an intersection ratio, and state used for notification. The bounded lab verifies those geometry-derived quantities and an application action policy. It does not measure a person looking at the target.
The fixture also intentionally excludes occlusion. Another element could theoretically paint over the target in a different page without changing the simple root/target intersection rectangle used here. No conclusion about unobscured pixels follows from these ten cases. Likewise, there is no continuous-time requirement: the test samples delivered entries around discrete geometry states. A ratio of 0.75 in I3 says nothing about whether the target remained at or above 0.5 for one millisecond, one second, or any other duration.
Do not turn the three-second test timeout into a latency statement. It exists so an expected observation failure terminates instead of hanging the suite. The run finishing in roughly 1.5 seconds is not a browser performance benchmark, and the entry time values are not being used to certify delivery deadlines.
The same caution applies to “exposure.” A component can have a valid reason to start work when 50% of its geometry intersects an explicit root (for example, an internal activation policy) but this test does not prove user-visible exposure, attention, dwell, or continuous presence. Those are different acceptance questions and require different evidence.
Do not convert a geometry test into an impression guarantee
This article therefore makes no advertising viewability certification, billing-impression claim, or accessibility conclusion. It does not use trackVisibility, does not attempt occlusion measurement, and does not measure continuous duration. Those topics are explicitly outside the fixture.
The correct wording is precise: “Under the recorded Chromium build and bounded layout, the delivered entry met the component’s 50% intersection-ratio policy.” Avoid stronger phrases such as “the user saw it,” “the impression was viewable,” or “it remained visible.” The test did not establish them.
This is also where HOLD is preferable to an invented answer. When a stakeholder’s requirement is actually “run only after the user has had an unobscured view for N milliseconds,” neither I3 nor I7 closes that requirement. Keep the geometry result, but hold the broader claim until an explicitly designed test exists.
Decide ACCEPT, REPAIR, RERUN or HOLD
Treat the decision labels as release controls, not prose summaries.
Decision | Use it when | Required owner/action |
ACCEPT | Effective configuration is the reviewed one, required browser/layout evidence exists, and each action is gated by entry.isIntersecting && entry.intersectionRatio >= 0.5 under this tested geometry. | Component owner approves the action policy; test/platform owner retains the evidence. |
REPAIR | Code dispatches on callback occurrence, dispatches on isIntersecting alone for a 50% rule, or passes thresholds instead of constructor threshold. | Component owner repairs the predicate; configuration owner repairs and verifies observer options; rerun before acceptance. |
RERUN | Browser executable/version is unknown, a supported browser has no ledger, root/target layout changed, transforms/clips/borders/padding/nested frames were introduced, or the source under test is not the reviewed artifact. | Integration-test owner records a new manifest and complete case evidence for the changed environment. |
HOLD | Claim requires user-visible exposure, dwell, occlusion, continuous visibility, or cross-browser parity that this fixture did not test. | Product/specification owner must define the missing requirement; publication or release claim waits for matching evidence. |
The labels prevent a frequent category error: repairing code when evidence is actually missing. Unknown browser version is not a REPAIR; it is a RERUN. A misspelled option with runtime [0] is not a RERUN-only problem; it is a REPAIR followed by a rerun. A demand for dwell time is not something this test can repair; it is a HOLD on that claim.
Use the labels in code-review and release notes only with the evidence they reference. “ACCEPT” is scoped to this component policy and tested geometry; it is not an endorsement of every IntersectionObserver behavior in the engine. “REPAIR” should name the defective decision rule or option. “RERUN” should name the missing environment or changed layout. “HOLD” should name the untested product claim so it cannot silently reappear as accepted copy.
Maintain browser-specific evidence and component ownership
Keep the ledger versioned with the browser executable, runtime metadata, fixture hash, start/finish timestamps, and assertion status. If Chromium changes its isIntersecting behavior in a later revision, preserve the old record and add a new one. Do not rewrite the historical row to look like the new engine, and do not treat the older implementation as normative merely because your production code depended on it.
The same rule applies when adding Firefox, WebKit, or another Chromium build: run the fixture there and record what that engine actually delivers. The W3C issue history shows why assumption-based cross-browser parity is risky; issue #432 contains years of discussion around the relationship between geometric intersection, thresholds, notifications, and implementation behavior. One Chromium ledger cannot authorize a statement that all browsers produce the same flags or notification sequence.
This discipline fits within broader frontend engineering skills, but the operational test should remain narrow. It is not a trend survey, performance benchmark, lazy-loading tutorial, or framework recipe. The maintainable artifact is the contract plus evidence: explicit root, explicit target geometry, effective thresholds, rectangles, ratio oracle, browser flag, action decision, and known exclusions.
Ownership should also stay explicit. Product or component owners define what “eligible” means; here that is at least 50% plus the reviewed intersecting flag. Frontend platform or integration-test owners define how that policy is reproduced and how configuration is verified. Browser-support owners decide which executable/version matrix must be rerun before a cross-browser claim is allowed. No single green boolean should silently answer all three questions.
Retention is useful only if a later engineer can reproduce what the row meant. Store the fixture source or immutable artifact reference with the manifest, not just the JSON output. Keep the browser executable identity separate from automation-library identity, and record whether the root or target CSS changed between runs. When evidence is regenerated, compare ledgers case by case instead of replacing an old PASS string with a new PASS string; the divergence columns may change even when action eligibility remains stable.
Finally, preserve the historical exploratory timeout as investigation evidence, not as one of the ten final observations. The final lab deliberately changed how it sampled 25% and edge contact (new observers were used) after the original outside-to-25% expectation did not produce a record in the tested Chromium build. Keeping that distinction protects future maintainers from converting an unobserved transition into fabricated data.
Practice browser validation with the FrontEnd Developer Program
The most transferable skill in this playbook is not memorizing one IntersectionObserver quirk. It is learning to turn a browser-dependent assumption into a bounded contract, measure the geometry independently, read back effective configuration, preserve negative cases, and separate observed implementation behavior from a standards draft.
Refonte Learning’s FrontEnd Developer Program page describes a curriculum centered on HTML, CSS, JavaScript, React, responsive interfaces, projects, guidance, and potential internship participation; it also lists basic programming knowledge as a requirement, with HTML/CSS familiarity as a plus. The page states that successful completion can lead to a Training Certificate and Certificate of Internship.
For engineers building confidence in browser APIs, the relevant practice is evidence-driven frontend work: define the contract first, then make the browser prove or disprove the assumption. That habit is what turns “the callback fired” from a vague success signal into evidence a team can review, rerun, and safely act on.
