fix: bring faint text up to WCAG AA contrast - #19
Merged
Merged
Conversation
`faint` was #5b626c, which fails 4.5:1 on every ground it is used on: 3.09:1 on `base`, 2.89:1 on `panel`, and 2.65:1 on `raised`, the lightest and so the binding one. The type scale tops out at 15px and nothing using the token is bold, so WCAG's 3:1 large-text allowance applies to none of the 34 sites. #808791 is the smallest lift that clears AA on all eleven grounds the token is read against, including the alpha-composited ones (`panel/40`, `raised/40`, `raised/60`, `warn/5`, `human/5`, `agent/5`), computed by compositing in gamma space the way a browser does. Worst case is now 4.51:1 on `raised`. Being the smallest lift, it is also the value that keeps the most separation from `muted` while passing — though 11 per channel is much less than the 48 the ramp had, so the two secondary tones now read as close to one. Three WCAG 1.4.11 failures go with it, since the token is also 13 borders and 4 fills: the idle status light was 2.89:1, the hover playhead line 2.11:1 and `hover:border-faint` on `raised` 2.65:1, against the 3:1 a UI boundary needs. One site is not fixed and cannot be fixed here. The rejected hypothesis card carries `opacity-60`, which veils text and ground together, so no foreground value can clear AA inside it: `faint` reaches 2.61:1 and `muted` — the card's own body text — only 2.90:1. It is an `opacity` problem rather than a token one, and the card keeps a live `promote` button, so 1.4.3's inactive-component exemption does not cover it either. Left for a change that can decide how the card should recede instead. The contrast note in replay-stage.tsx quoted faint's old 3.09:1 as the reason it picked `muted`; that measurement is no longer true, so the note now gives the margin over replayed page content as the reason instead.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This branch was successfully deployed
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-faintwas#5b626c, which fails WCAG AA on every ground it is used on. This raises it to#808791, the smallest value that clears 4.5:1 on all eleven grounds — one site excepted, see thebottom.
Method
Ratios computed from sRGB relative luminance, with alpha-composited grounds flattened in gamma
space, which is what a browser does for
bg-panel/40and friends. The existing note inreplay-stage.tsxindependently quotedfaintat 3.09:1 onbg-base; the calculator reproduced thatfigure exactly, which is the check that the method is right.
The 3:1 large-text allowance applies nowhere.
fontSizeintailwind.config.tstops out attitle= 15px, and no site using the token is bold, so every one of the 34 is held to 4.5:1.Text — before and after, per (colour, ground) pair
Text is
faintin every row; only the ground varies. Sizes are from the role-named scale(
micro10px,label11px,meta12px,body13px).baseagent-lane:113mono 11 normal ·agent-lane:14413 normal (placeholder) ·agent-lane:20112 normal ·agent-lane:212icon 13 ·ask-human:174,:17812 normal ·report-draft:14713 normal (placeholder) ·report-draft:188mono 11 normal (even rows) ·report-draft:202mono 11 normal (aria-hidden) ·report-draft:30912 normal ·report-draft:32411 medium ·report-draft:34111 normal ·stage-empty-state:7312 normal ·bisect-trace:327mono 11 normal ·timeline:12512 normal ·section-heading:63icon 15panelpage:21911 normal ·activity-feed:81mono 11 normal ·agent-lane:84mono 11 normal (donechip) ·player-controls:139icon, disabled ·player-controls:154mono 12 normal ·recording-picker:357icon 14, active row ·recording-picker:380mono 11 normal, active row ·section-heading:63icon 15 ·webmcp-badge:15311 normal ·webmcp-badge:15810 normalraised(binding)player-controls:22511 normal ·player-controls:281mono 11, disabled ·hypothesis-cards:179icon 11 ·recording-picker:357icon 14 ·recording-picker:380mono 11 normalpanel/40onbasereport-draft:188mono 11 normal (odd rows) ·hypothesis-cards:124,:18911 normal (proposed)panel/40onwarn/5raised/40onbasestage-empty-state:114,:150icons 16raised/60onbase(hover)agent-lane:212,stage-empty-state:114,:150on hoverwarn/5onbaseagent-lane:183mono 11 normal (claimed row) ·report-draft:147,:309,:324while awaitinghuman/5onbasehypothesis-cards:124,:18911 normal (promoted)human/5onpanelactivity-feed:131mono 11 normal (your rows)agent/5onpanelactivity-feed:131mono 11 normal (agent rows)Every pair passes.
raisedis what sets the value: at anything dimmer than#808791it drops below4.5:1, and
#808791is the first value that clears it.Why the smallest lift is also the right one for the ramp
The token lands 11 per channel from
muted(#8b929c), where it used to sit 48 below it. The three-tonetext ramp therefore reads as closer to two tones in places. That is a real cost, but it is unavoidable
rather than chosen: any value dark enough to keep the old spacing fails on
raised, and any valuelighter collapses the pair further. The minimum lift is simultaneously the maximum separation
available with both tones passing. Re-spacing
mutedupward to restore three distinct steps is adesign call, and a separate change.
Non-text uses come along, and three of them were also failing
The token is 34 text uses, 13 borders and 4 fills. Borders and fills are held to 3:1 by WCAG 1.4.11:
DOT.idlestatus light,bg-faintonpanel(tool-status-banner:99)bg-faint/70onbase(timeline:235)hover:border-faintonraised(7 sites)bg-faintonbase(bisect-trace:244)bg-faintonbase(resizable-split:152)hover:border-faintonbase(6 sites)Sites that do not drive the token
player-controls:139and:281—disabled:text-fainton a disabled control. WCAG 1.4.3 exemptsinactive components, so these could not have forced a change. They pass anyway now (4.91:1, 4.51:1).
section-heading:63,report-draft:202, and the six icons —aria-hiddenglyphs whose meaning iscarried by adjacent text, so 1.4.11 does not bind them either. Also pass anyway.
Neither group was used to justify the value;
raiseddid that on its own, via readable text.One site is not fixed, and no value of this token can fix it
hypothesis-cards.tsx:66gives a rejected hypothesis cardbg-panel/20 opacity-60. CSSopacityis agroup veil: it multiplies the text and the card's ground toward the backdrop together, so the ratio
inside is capped no matter what the foreground is.
faintbeforefaintaftermuted— the card's own hypothesis textinkTwo things follow. First, this is an
opacity-60problem, not afaintproblem: the card's body text istext-muted line-throughand fails on its own, so darkening or lighteningfaintneither causes nor curesit. Second, WCAG 1.4.3's inactive-component exemption does not apply — I checked, and
Verdictisdisabled={active}, so on a rejected card thepromotebutton is still live and titled "Change the recordto promoted." The card is active content, not a disabled control.
Fixing it means deciding how the card should recede without a group veil — explicit dimmer colours, or a
different treatment entirely. That is a design decision about one component's status treatment, so it does
not belong in a commit that moves a shared token, and I have left it. This PR does not bring that one card
to AA; the config comment records why, so the next person does not read
#808791as a claim about it.Verification
npx tsc --noEmit— exit 0npm run lint— 0 errors, 8 warnings (the same 8 pre-existing since fix: make npm run lint work in a clean clone #16; none in the touched files)npx vitest run— 25 files, 303 tests passed, unchanged from baselineNote on the second file
replay-stage.tsxis a comment-only edit, and it is here because this change falsified it: it justifiedpicking
mutedoverfaintby quotingfaintat 3.09:1, which is no longer its value. The note now givesthe surviving reason — that readout is read against replayed page content, not just
bg-base, so it wantsthe larger margin — and quotes both current figures.