Repository navigation
Bring every colour pair to 4.5:1, and keep it there - #283
Merged
Merged
Conversation
Eight tokens failed WCAG AA against surfaces they are actually painted on. Measured, not inspected: dark --text-muted on --bg-elevated 4.13 settings dialog descriptions dark --text-muted on --bg-selected 3.36 dark --red on --bg-selected 3.76 "Down" on a hovered row light --mint on --bg-selected 3.88 accent, as text, everywhere light --text-muted on --bg-body 4.19 light --amber on --bg-base 4.34 light --cyan on --bg-body 4.45 The light theme had drifted furthest, which is what you would expect of the theme nobody looks at. Two of these are in code merged today: the address line in the picker is --text-muted on the dialog, and the red "Down" label is what a hovered down interface renders. Each token moves in lightness only -- hue and saturation are untouched, so the palette reads the same. The values are the minimum that clears 4.6:1 against every surface, found by search rather than by taste. scripts/design-tokens.mjs is the guard. It reads tokens.css, checks the full foreground x surface cross-product, and generates design-tokens.json for the frontend checker. Two decisions worth stating: - The JSON is generated, never hand-written. tokens.css is the single source of truth, and a hand-maintained copy drifts; a drifted copy is worse than none, because the contrast gate then passes against values that are not what ships. The check fails if the committed JSON no longer matches. - surface-base maps to the tightest surface per theme, not the nominal base one. bg-base scored --text-muted at 4.54 while bg-elevated -- the settings dialog's own background -- was at 4.13, so mapping the easy surface would have passed the gate with the dialog still unreadable. The cross-product is checked rather than an audited list of pairs real rules combine: the audited list needs updating whenever a rule pairs a new colour with a new surface, and that update is what gets forgotten. It is also what found --red on --bg-selected, which was not on my list.
fstubner
added a commit
that referenced
this pull request
Aug 29, 2026
The guard I added in #283 passes CI and fails on every Windows working copy. `.gitattributes` sets `* text=auto`, so `design-tokens.json` is checked out CRLF here and LF on the Ubuntu runner, while the generator always writes LF. The byte-for-byte comparison then reports the file as out of date on the maintainer's own machine and green where nobody is looking. That is the failure the script's own header argues against, one layer up: a check that is green in the place it is read and red in the place the work happens teaches people to ignore it. Compared normalised now. Verified both directions -- CRLF and LF copies both pass, and a genuinely drifted value is still caught under both.
fstubner
added a commit
that referenced
this pull request
Aug 29, 2026
) site/design-tokens.json was hand-written -- the exact defect #283 argued against, on the other surface. The reasoning at the time was that values reached through --sl-color-text and other indirections are not always the declared one, so they were measured from a served build instead. That concern was real but did not apply here: generating the file reproduced eight of its ten hand-measured values byte for byte, including the whole dark theme and the accent that resolves through `var(--netscli-accent)`. The two that changed are deliberate: - light text-muted #667387 -> #647084. --sl-color-gray-3 is the docs footer, built-with row and mobile section labels, much of it at 12px. It had been raised once already, to clear 4.5:1 against the page background, but nothing checked it against --sl-color-gray-6, the card surface, where it sat at 4.44:1. It now clears every light surface. - light surface-base now maps to gray-6 rather than bg, matching the app's rule of pinning the gate to the tightest surface rather than the flattering one. Mapping bg is what let 4.44:1 go unnoticed. One script now covers both surfaces rather than duplicating the contrast maths. Two parser changes were needed for the site: selector-list matching, because Starlight's tokens live on `html[data-theme='dark'], html[data-theme='dark'] ::backdrop {` which an anchored match misses entirely; and one level of var() resolution, for `--sl-color-accent: var(--netscli-accent)`. The check now runs in site.yml as well as ci.yml. ci.yml gates it on `code`, which a site-only pull request sets to false, so the site's own contrast could otherwise regress with nothing looking at it. It takes milliseconds, so neither workflow can be the one that forgot.
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.
F-tokens-contrasthad been sitting atnot_evaluatedbecause the app has nodesign-tokens.json. Extracting one turned out not to be paperwork: eight tokens fail WCAG AA against surfaces they are actually painted on.What was measured
Not the theoretical cross-product — these are pairs real CSS rules put together:
--text-mutedon--bg-elevated--text-mutedon--bg-selected--redon--bg-selectedDownlabel on a hovered interface row--minton--bg-selected--text-mutedon--bg-body--amberon--bg-base--cyanon--bg-bodyThe light theme had drifted furthest, which is what you would expect of the theme nobody looks at. Two of these are in code merged earlier today (#282): the picker's address line is
--text-mutedon the dialog background, and the redDownlabel is exactly what a hovered down interface renders.The fix
Each token moves in lightness only — hue and saturation untouched, so the palette reads the same. Values are the minimum that clears 4.6:1 against every surface, found by search rather than by taste. All 84 foreground×surface combinations now clear 4.5:1.
The guard
scripts/design-tokens.mjsreadstokens.css, checks the cross-product, and generatesdesign-tokens.json. Runs in CI and asnpm run test:tokens.Two decisions worth arguing with:
tokens.cssis the single source of truth; a hand-maintained copy drifts, and a drifted copy is worse than none — the contrast gate then passes against values that are not what ships. The check fails if the committed JSON no longer matches its source. (site/design-tokens.jsonis hand-written and has this defect; it should get the same treatment separately.)surface-basemaps to the tightest surface per theme, not the nominal base one.--bg-basescored--text-mutedat 4.54 while--bg-elevated— the Settings dialog's own background — was at 4.13. Mapping the easy surface would have passed the gate with the dialog still unreadable.The cross-product is checked rather than an audited list of real pairs. An audited list needs updating whenever a rule pairs a new colour with a new surface, and that update is what gets forgotten. It is stricter than reality, currently satisfied, and needs no maintenance — and it is what found
--redon--bg-selected, which was not on my hand-written list.Verification
--text-mutedmakes the guard fail with all three of its dark-theme pairs named, and flags the JSON as out of date. Restored, green again.F-tokens-contrast: 4 pair(s) >= 4.5:1 across 2 theme(s), up fromnot_evaluated.tscexit 0,eslintclean, file-size guard clean, full Tauri render suite green (it asserts themes, and these are colour changes).