diff --git a/packages/cli/src/commands/contrast-audit.browser.js b/packages/cli/src/commands/contrast-audit.browser.js
index 0aa7efe505..d3215ddf07 100644
--- a/packages/cli/src/commands/contrast-audit.browser.js
+++ b/packages/cli/src/commands/contrast-audit.browser.js
@@ -361,17 +361,25 @@ window.__contrastAuditFinish = async function (imgBase64, time, candidates) {
var stepY = Math.max(1, Math.floor((y1 - y0) / 6));
var rr = [],
gg = [],
- bb = [];
+ bb = [],
+ aa = [];
for (var y = y0; y <= y1; y += stepY) {
for (var x = x0; x <= x1; x += stepX) {
var idx = (y * w + x) * 4;
rr.push(px[idx]);
gg.push(px[idx + 1]);
bb.push(px[idx + 2]);
+ aa.push(px[idx + 3]);
}
}
if (rr.length === 0) continue;
+ // A transparent sampled backdrop is supplied by the downstream editor,
+ // not this composition. WCAG contrast cannot be inferred until that live
+ // video/image is known, so do not invent an opaque browser default and
+ // hard-fail an otherwise valid alpha overlay.
+ if (median(aa) < 255) continue;
+
var bgR = median(rr),
bgG = median(gg),
bgB = median(bb);
diff --git a/packages/cli/src/commands/layout-audit.browser.test.ts b/packages/cli/src/commands/layout-audit.browser.test.ts
index 821bad8c14..2c47f6df14 100644
--- a/packages/cli/src/commands/layout-audit.browser.test.ts
+++ b/packages/cli/src/commands/layout-audit.browser.test.ts
@@ -605,6 +605,36 @@ describe("contrast-audit.browser background sampling", () => {
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({ selector: "#label", wcagAA: true, bg: "rgb(10,10,10)" });
});
+
+ it("skips text whose sampled backdrop remains transparent", async () => {
+ document.body.innerHTML = `
+
+ Live
+
+ `;
+
+ vi.spyOn(window, "getComputedStyle").mockImplementation(
+ () =>
+ ({
+ display: "block",
+ visibility: "visible",
+ opacity: "1",
+ color: "rgb(255, 255, 255)",
+ fontSize: "20px",
+ fontWeight: "700",
+ clipPath: "none",
+ }) as unknown as CSSStyleDeclaration,
+ );
+ vi.spyOn(document.getElementById("label")!, "getBoundingClientRect").mockReturnValue(
+ rect({ left: 50, top: 50, width: 100, height: 30 }),
+ );
+ (document as unknown as { elementFromPoint: () => Element | null }).elementFromPoint = () =>
+ null;
+
+ installContrastScript(new Uint8ClampedArray(640 * 360 * 4));
+
+ expect(await runContrastAudit()).toEqual([]);
+ });
});
// Both blocks overlap heavily; only the exemption on block A should suppress
diff --git a/packages/cli/src/commands/validate.ts b/packages/cli/src/commands/validate.ts
index 9624615717..2610aa725e 100644
--- a/packages/cli/src/commands/validate.ts
+++ b/packages/cli/src/commands/validate.ts
@@ -276,7 +276,11 @@ async function runContrastAudit(page: import("puppeteer-core").Page): Promise
typeof (window as unknown as Record).__contrastAuditFinish === "function"
diff --git a/packages/cli/src/utils/checkBrowser.ts b/packages/cli/src/utils/checkBrowser.ts
index d889e41b73..8dd7a47fa6 100644
--- a/packages/cli/src/utils/checkBrowser.ts
+++ b/packages/cli/src/utils/checkBrowser.ts
@@ -510,7 +510,11 @@ async function collectContrast(
// This screenshot is the one contrast math is sampled from below — it must
// stay untouched by the annotation overlay (finishContrast reads real
// painted pixels), so annotation only ever happens on a SECOND shot.
- const measurementShot = await page.screenshot({ encoding: "base64", type: "png" });
+ const measurementShot = await page.screenshot({
+ encoding: "base64",
+ type: "png",
+ omitBackground: true,
+ });
if (typeof measurementShot !== "string") throw new Error("Contrast screenshot was not base64");
const raw = await finishContrast(
page,