Skip to content

fix(cli): contrast audit reads an element's own opaque background#1975

Draft
miguel-heygen wants to merge 1 commit into
mainfrom
fix/contrast-audit-opaque-own-background
Draft

fix(cli): contrast audit reads an element's own opaque background#1975
miguel-heygen wants to merge 1 commit into
mainfrom
fix/contrast-audit-opaque-own-background

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

Problem

hyperframes validate's WCAG contrast audit estimates each text element's background by sampling a 4px pixel ring just outside its bounding box (contrast-audit.browser.js). For an element that paints its own opaque background (a caption pill, a CTA button, a solid card), the text is composited over that solid color, not over whatever surrounds the box. Sampling the ring there measures the text against the scene behind the element (often a dark photo), producing false ~1:1 ratios and flagging perfectly readable CTAs/captions.

The tell users kept hitting: changing the element's background color never moved the reported ratio (only changing the text color did) — because the audit was never reading the background it complained about.

Fix

Resolve the nearest fully-opaque background-color by walking the element up its ancestor chain, and use it as the background when present. Keep sampling the pixel ring only when the text genuinely sits over image pixels (a background-image is encountered first) or no opaque background-color exists. A semi-transparent background-color is skipped (it blends with what's below, which the ring captures better).

The pure decision lives in a new packages/cli/src/commands/contrast-bg.ts with unit tests. contrast-audit.browser.js is injected into the page as a raw string (it cannot import), so it inlines the same logic — mirroring the existing "WCAG math is duplicated, keep in sync" note already at the top of that file.

Verification

packages/cli/src/commands/contrast-bg.test.ts — 8 assertions covering: own opaque bg used; transparent element → opaque ancestor used; background-image first → defer to ring (null); semi-transparent skipped; nothing opaque → null. All pass. Existing over-image behavior is unchanged (null path → same ring sampling as before).

The WCAG contrast audit estimated each text element's background by sampling a
4px pixel ring just OUTSIDE its bounding box. For an element that paints its
OWN opaque background (a caption pill, a CTA button, a solid card), the text is
composited over that solid color, not over whatever surrounds the box. Sampling
the ring there measured the text against the scene behind the element (often a
dark photo), producing false ~1:1 ratios and flagging perfectly readable CTAs
and captions. Users reported the warning persisting no matter how they changed
the background color, because the audit was never reading it.

Resolve the nearest fully-opaque background-color by walking the element up its
ancestor chain, and use it when present; keep sampling the ring only when the
text sits over image pixels (a background-image is hit first) or no opaque
background exists. The pure decision lives in a new commands/contrast-bg.ts with
unit tests; contrast-audit.browser.js (injected as a raw string, so it cannot
import) inlines the same logic, mirroring the existing duplicated-WCAG-math
note.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant