Closed Bug 2068294 Opened 1 month ago Closed 7 days ago

[meta] Enforce paired background and text color design tokens

Categories

(Developer Infrastructure :: Lint and Formatting, task)

task

Tracking

(Not tracked)

RESOLVED FIXED

People

(Reporter: dao, Assigned: dao)

References

(Blocks 3 open bugs)

Details

(Keywords: access, meta)

This adds a stylelint rule requiring that a block painting a background design token also declares its paired text color. It went in as two rules, each landing with a tree-wide sweep that fixed the existing violations, and they have since merged into one rule that can also insert a missing declaration itself. Every background variant in a family with a base text color now has a text counterpart, and a test keeps it that way. Checking that a surface's text contrasts with what it actually renders on is a separate effort.

Background and text tokens come in pairs that share a name apart from the background-color / text-color part, so --button-background-color-primary-hover goes with --button-text-color-primary-hover. A pair is meant to contrast in every theme, in dark mode, under the prefers-contrast media query and under forced colors. Mixing halves from two different pairs breaks that, and so does painting a background and leaving the text color to whatever ancestor happens to supply one.

Neither failure is visible to most Nightly users, so a regression goes unnoticed for a while. In the default theme most of these surfaces render identically whether the pairing is right or wrong, because the button background token is a translucent color-mix of the inherited text color. The mismatch may become visible in forced colors mode or under specific lightweight themes.

Theme reviews do catch these most of the time, but not always. The check is mechanical; it recurs on every patch that touches a color, and a stylelint rule makes it exhaustive and free.

Scope

  • The rule checks one direction only, from a painted background to the text color it owes. It does not require a background where a block declares only a text color: 19 of the 23 such sites are descendants of the element painting the background, which the rule cannot see.
  • The rule does not check a block that redefines one half of a pair for its subtree and leaves the other at its stock value. A name check would report 160 such sites. A contrast audit of all 160 found 30 that fail, and the name check would miss 4 of them. Only a contrast check on the rendered surface sees them. The 30 are filed per component as bug 2078754, bug 2078755, bug 2078756, bug 2078757, bug 2078758 and bug 2078759.
  • Nothing here checks that a pair contrasts, samples rendered pixels or covers a surface built from literal colors. The token table cannot answer it for most pairs, whose background is translucent or mixes in the text color, so the answer depends on what the surface renders on.

Bugs caught so far

This is a running list of what the rules flagged when landing.

  • The app menu's sync-setup button — wrong text color against its background in light mode, visible in the default theme; fixed as part of bug 2068024
  • about:welcome's selection flair — white text on a pale violet fill; fixed as part of bug 2068024
  • The Top Sites edit dialog — took the CanvasText system color from the browser's built-in stylesheet instead of the panel's text token; fixed as part of bug 2068024
  • The urlbar result-menu button in Proton, the shipped visual design at the time, took its text color from the row it sits in, where the Nova variant of the same button already declared the counterpart; fixed as part of bug 2068024
  • The urlbar box's active state — six sites reached for the hover text token because no active one existed. The fix was a new token, and it resolves to the same value in every branch that matched, so nothing rendered differently. Fixed as part of bug 2067353
  • Two unrelated defects in the AI window — local variables reusing another widget's design token names, and a hardcoded radius where a token exists. Neither is a contrast bug; reading the CSS closely turned them up. Filed as bug 2068078 and bug 2068080
  • Twelve nested hover and active states — a hover or active button background repainted inside the rule that declares the base text color, in the app menu, the translations panel, toolkit's shared button styles, an onboarding screen and two New Tab widgets. Under forced colors the base text is ButtonText and the state text SelectedItem, so each state inherited the wrong one. Fixed in bug 2074413
  • The vertical tab audio overlay in an inactive window — painted the selected tab's text color on the inactive toolbox background, where the horizontal overlay already took the inactive toolbox text color. Under native themes and forced colors the icon took the wrong system color. Fixed in bug 2079750

Progress at a glance

Phase Goal Bug Status
1 Create use-paired-color-tokens rule bug 2067353 DONE
2 Create the no-background-without-text-color rule bug 2068024 DONE
3 --fix for automatically adding the missing text color bug 2068773 DONE
4 Merge no-background-without-text-color into use-paired-color-tokens bug 2069552 DONE
5 Also check :hover and other states against the base rule bug 2074413 DONE
6a Let every background variant have a text counterpart to pair with bug 2075554 DONE
6b Retire the dimmed background tokens since they lack high contrast values bug 2075556 DONE
7 Count background-image as painting a background bug 2079750 DONE

Running the linter

The rule runs over the whole tree, and is not staged behind a rollout entry.

  • Lint a path, or the tree: ./mach lint -l stylelint <path>. The whole tree takes about 30 seconds, and the tier-1 CI task runs the same command.
  • Autofix what is fixable: ./mach lint -l stylelint --fix <path>. The autofix rewrites a mispaired color and inserts a missing one after the declaration that paints the surface. It reports without fixing only when a comment ends that declaration's line, since the insertion would take the comment onto the new line. stylelint honours a disable comment for fixes as well as for reports, so --fix leaves an annotated surface alone rather than inserting a color the site has already argued against.
  • The rule's own tests: ./mach npm test --prefix tools/lint/stylelint/stylelint-plugin-mozilla.

When a surface genuinely should not have a text color, the disable comment has to say why, in terms a reviewer can check:

/* The tracker count below takes the box text color; a color here would recolor the shield. */
/* stylelint-disable-next-line stylelint-plugin-mozilla/use-paired-color-tokens */
background-color: var(--urlbar-box-background-color);

A deferred fix gets a TODO naming a filed bug instead. Check the alternative before reaching for either. A text color costs one line and stays true, whereas a justification that reasons about what the block contains — no text here, an icon takes its color from below, a child colors itself — is almost always falsifiable by a markup change, or by forced colors turning a translucent color-mix opaque.

References

Depends on: 2068773
Depends on: 2069552
Assignee: nobody → dao+bmo
Status: NEW → ASSIGNED
Depends on: 2074413
Depends on: 2075554
Depends on: 2075556
Depends on: 2079750
Status: ASSIGNED → RESOLVED
Closed: 7 days ago
Resolution: --- → FIXED
You need to log in before you can comment on or make changes to this bug.