fix(#462): text dimmed to 70% or below fails AAA on every surface — remove it, then guard it - #800
Merged
Merged
Conversation
…emove it, then guard it #411 measured `text-base-content/60` and `/70` failing the 7:1 AAA gate and removed them. They came back. Eight live instances in `src/twin/cesium/` plus two in a story, unnoticed for months -- and nothing could have noticed: #459 records that axe-core returns a PASS with `contrastRatio: null` on these nodes, so `color-contrast.spec.ts`, which asserts on `violations`, has been green on them throughout. Measured (#462, canvas readback because getComputedStyle returns oklch() unparsed), `scripthammer-light`, gate 7:1: /100 /85 /80 /70 base-100 12.96 8.30 7.08 5.20 base-200 11.73 7.73 6.62 4.98 base-300 10.10 6.88 6.05 4.57 `/70` and below fails on EVERY surface, with no judgement call about which surface the text landed on. That is what is removed here. The instances found were worse than that table, which assumes OPAQUE surfaces: they sat on `bg-base-100/90` with `backdrop-blur`, floating over the Cesium canvas at 10-11px, so the effective background was an arbitrary 3D scene. That is the #715 situation, where the answer was solid text over a raised surface. Replaced with solid `text-base-content`. The de-emphasis survives -- these are already 10-11px next to 13px+ labels, and size costs no contrast. VERIFIED THE SYMPTOM FIRST. The five instances #462 originally named (MessagingGate, MessageThread, messages/new-group, messages/setup) are all already clean; the issue as written did not reproduce. The real finding was elsewhere, and #462 is rewritten around what is actually left: 184 `/80` and `/85` across ~100 files, whose safety is surface-DEPENDENT (`/80` passes on base-100 at 7.08 -- a 0.08 margin -- and fails on base-200 at 6.62). That needs a per-call-site surface determination and is not this change, so this is `Refs`, not `Closes`. GUARDED by scripts/__tests__/text-opacity-aaa-floor.test.js, in `pnpm test:scripts` and so inside the required `Test (20.x)` check. A removal without a guard is a removal that gets undone -- which is the whole history here. It exempts genuinely decorative content, because WCAG does: `aria-hidden="true"`, or `<Icon … decorative />`, which Icon.tsx:61 turns into `aria-hidden`. It does NOT accept a comment claiming decorativeness -- `docs/page.tsx` carries exactly such a comment and it is the prop six lines below that actually hides the glyph. Accepting prose would let a stale comment exempt live markup. `globals.css` is allowlisted WITH ITS REASON (code-block line numbers, `select-none`), flagged in #462 as arguably informational rather than decorative -- recorded, not silently skipped. Mutation-verified with the mutant confirmed in the file first: restoring `/60` at BuildingCard.tsx:151 fails the guard naming that exact file:line. Controls prove the detector fails in both directions, that `aria-hidden` 20 lines away does NOT exempt, and that prose containing the word "decorative" does not either. The first sed for this change silently did nothing -- `|` delimiter against `\|` alternation, the exact trap CLAUDE.md documents. Caught only by comparing before/after counts, which is why that check is in the workflow rather than trust. type-check clean, lint clean, component tests 33 passed, `pnpm test:scripts` 363 passed. Refs #462, #411, #459, #715
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
text-base-contentat 70% opacity or below fails the 7:1 AAA gate on every surface in thelight theme. #411 measured that and removed it. It came back, and nothing noticed for months.
Full finding in #462, which is rewritten around what remains rather than what this fixes.
Why nothing noticed
#459: axe-core returns a pass with
contrastRatio: nullon these nodes, socolor-contrast.spec.ts— which asserts onviolations— has been green on them the wholetime. A removal without a guard is a removal that gets undone, and that is exactly what
happened between #411 and today.
The measurement (#462, canvas readback —
getComputedStylereturnsoklch()unparsed)scripthammer-light, gate 7:1:/100/85/80/70base-100base-200base-300/70and below fails everywhere, with no judgement call about which surface the text landedon. That is what this removes.
The instances were worse than the table
All eight real ones sat in
src/twin/cesium/onbg-base-100/90withbackdrop-blur,floating over the Cesium canvas at 10–11px. The table assumes opaque surfaces, so a
translucent panel over an arbitrary 3D scene is unbounded-worse than its worst row — the #715
situation, where the answer was solid text on a raised surface.
Replaced with solid
text-base-content. The de-emphasis survives: these are already 10–11pxbeside 13px+ labels, and size costs no contrast.
Verified the symptom first
The five instances #462 originally named are all already clean —
MessagingGate,MessageThread,messages/new-group,messages/setup. The issue as written did notreproduce. The real finding was elsewhere, which is why #462 is rewritten rather than closed.
Why this is
Refsand notCloses184
/80and/85remain, across ~100 files, and they cannot be swept mechanically —/80passes onbase-100(7.08, a margin of 0.08) and fails onbase-200(6.62). Thatneeds a per-call-site surface determination and is its own piece of work.
The guard
scripts/__tests__/text-opacity-aaa-floor.test.js, inpnpm test:scriptsand therefore insidethe required
Test (20.x)check.It exempts genuinely decorative content, because WCAG does —
aria-hidden="true", or<Icon … decorative />whichIcon.tsx:61turns intoaria-hidden. It does not accept acomment claiming decorativeness:
docs/page.tsxcarries exactly such a comment, and it is theprop six lines below that actually hides the glyph. Accepting prose would let a stale comment
exempt live markup.
globals.cssis allowlisted with its reason (code-block line numbers,select-none) andflagged in #462 as arguably informational rather than decorative — recorded, not silently
skipped.
Mutation-verified, mutant confirmed in the file first: restoring
/60atBuildingCard.tsx:151fails the guard naming that exact
file:line. Controls prove the detector fails in bothdirections, that an
aria-hidden20 lines away does not exempt, and that prose containingthe word "decorative" does not either.
One process note worth keeping
The first
sedfor this change silently did nothing —|delimiter against\|alternation, the exact trap CLAUDE.md documents. It was caught only by comparing before/after
counts (
6 -> 6), which is why that comparison is in the workflow rather than trust.Verified
type-check clean · lint clean · component tests 33 passed ·
pnpm test:scripts363 passedRefs #462, #411, #459, #715