Contrast: don't blend translucent backgrounds against white when an ancestor has a gradient/background image - #172
Conversation
getBackground() blended a translucent background colour against white
whenever the ancestor chain had no opaque background-color, even when an
ancestor carried a background-image or gradient. White text on a
rgba(...) pill over a dark gradient section was therefore reported as a
CONTRAST_ERROR with a ~1:1 ratio, although the rendered contrast is fine.
- Ancestor with background-image/gradient: return an image background
with the translucent colour as `overlay`; checkContrast blends the
overlay onto each gradient stop, so the element is reviewed like any
other gradient background (warning only if a stop fails, never a
false error against white).
- Several translucent layers on top of each other are now composited
("over" operator) before blending with the first opaque ancestor.
Previously the first translucent ancestor was treated as opaque,
which hid real failures (e.g. two rgba(255,255,255,.3) layers over
black with white text now correctly fail).
- Unit test fixtures: nothing-contrast-7, warning-contrast-2,
error-contrast-7.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Gradient stops with alpha (e.g. a decorative glow such as `radial-gradient(rgba(75,187,217,.16), rgba(75,187,217,0)), #313f4e`) were checked as if they were opaque, so white text on a dark section with a subtle glow was flagged for manual review, and even a fully transparent stop counted as a solid colour. getBackground() now records the opaque colour behind an image/gradient (`base`: the element's own background-color, or the first opaque ancestor after compositing translucent layers). checkContrast() blends each stop onto `base` before the check; a descendant's translucent background colour (`overlay`) is composited on top. When the colour behind the gradient is unknown (nested images), behaviour is unchanged. Fixtures: nothing-contrast-8, warning-contrast-3. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Pushed a follow-up commit (04a256d) after testing this on more pages. Second issue, same corner: gradient stops with alpha are checked as if they were opaque. A decorative glow like What the commit does: Results on the same page as above:
169 unit tests still green, two more fixtures added ( |
|
Thank you very much for the contribution, @skerbis! What a great fix! I'll include in next release! |
Hi Adam,
first of all: thanks for Sa11y, it's a great tool. I ship it to REDAXO CMS users via the for_sa11y addon, so editors get the checks right on their pages while working.
The problem
While going through a customer site I kept getting contrast errors with a ratio of about 1.1:1 on text that clearly looked fine: white text inside a "pill" with a translucent background like
rgba(75, 187, 217, .14), sitting on a section with a darklinear-gradientbackground.Turns out
getBackground()only walks up looking for an ancestorbackground-colorwhen the element's own background has alpha. Ancestors with abackground-image(gradients included) are skipped, so it falls back to white and blends the translucent colour against that. Hence white on white → 1.1:1 → error. The same page with axe-core was clean, since axe composites the layers, so I thought this should be fixable in Sa11y too.A second thing I noticed on the way: if there are two translucent layers on top of each other (e.g. a card with
rgba(255,255,255,.3)inside a wrapper withrgba(255,255,255,.3)on a black section), the first translucent ancestor is treated as opaque. That hides real failures.The fix
background-image/ gradient,getBackground()now returns an image background and carries the translucent colour asoverlay.checkContrast()blends that overlay onto each gradient stop, so the element is handled like any other gradient background: no issue if all stops pass, a "needs manual review" warning if one fails, and no false error against white anymore.stackTranslucent()) before blending with the first opaque ancestor.test/pages/unit-tests.html(nothing-contrast-7,warning-contrast-2,error-contrast-7) and the ids added to the spec.Test results
Compared the released 5.0.9 with this branch on the same real-world page, each in a fresh browser context:
rgba(255,255,255,.3)layers over black, white textnpx playwright test test/unit-tests.spec.js --project=chromium: 169 passed.The second commit is just the rebuilt
dist/(feel free to drop it if you prefer building on release).Thanks again for the great work on Sa11y!