Skip to content

[POC] Add color-contrast a11y regression suite + token tuning experiment#48530

Draft
siriwatknp wants to merge 15 commits into
mui:masterfrom
siriwatknp:a11y/color-contrast
Draft

[POC] Add color-contrast a11y regression suite + token tuning experiment#48530
siriwatknp wants to merge 15 commits into
mui:masterfrom
siriwatknp:a11y/color-contrast

Conversation

@siriwatknp
Copy link
Copy Markdown
Member

@siriwatknp siriwatknp commented May 12, 2026

Passed WCAG AA contrast ratio: https://deploy-preview-48530--material-ui.netlify.app/experiments/color-contrast-tokens/?info.light.main=%23077cbb&warning.light.main=%23cc4b05&error.dark.main=%23e72323&primary.light.main=%23146bc2

Goal

A POC to validate the color contrast fix as "bug fix" rather than "breaking change".

Problem

The current failed color contrast listed starting from https://github.com/mui/material-ui/pull/48530/changes#diff-9ad1ca0cb21c17e71518d753f177620e6ca97246954b093ce35e8f8e0a8033e2

tested both light and dark mode for components that has color prop.

  • info, warning main token is not creating enough contrast
  • ToggleButton and PaginationItem selected color need adjustment, tweaking only the main token is not enough because it has translucent background when selected.

Changes

  • Tweak the tokens as minimal as possible to achieve 4.5:1
  • adjust selected color value for ToggleButton and PaginationItem to be
- color: (theme.vars || theme).palette[color].main,
+ color: (theme.vars || theme).palette[color].dark,
+ ...theme.applyStyles('dark', {
+   color: (theme.vars || theme).palette[color].light,
+ }),

siriwatknp added 12 commits May 11, 2026 16:42
Alert extends Paper, which in dark mode (without CssVarsProvider) sets
`--Paper-overlay` to a degenerate `linear-gradient(...)` for the
elevation tint. axe's color-contrast rule treats any `background-image:
linear-gradient(...)` as unresolvable and returns `incomplete` with
`contrastRatio: 0`, hiding whether the actual fg/bg pair passes AA.

Override `MuiAlert.styleOverrides.root.backgroundImage = 'none'` in
both fixtures so the probe records measurable contrast numbers. With the
overlay suppressed, dark Alert passes AA cleanly; light Alert still
shows the known filled+info / filled+warning fails.
…d ColorContrast fixtures

Three different axe limitations were downgrading real contrast results
to 'incomplete' (contrastRatio: 0):

- Badge: bubble extends outside MailIcon's 24x24 bounds, triggering
  axe's 'elmPartiallyObscured' guard. Wrap MailIcon in a larger Box so
  the bubble fits entirely within the badge anchor.
- Pagination: single-digit page number 'shortTextContent' guard.
  Use renderItem to widen the page prop ('page N').
- TextField: floating label + outlined notched fieldset both overlap
  the input ('bgOverlap'). Force the label to stay above the input via
  slotProps.inputLabel.shrink and hide the fieldset for outlined.

Probe now records measurable contrast for every cell.
…t fixtures

The bare `inputProps={{ 'aria-label' }}` is dropped in v9, so the
rendered <input> had no accessible name and axe's `label` rule failed
on all cells. Wrapping each control in FormControlLabel renders a real
<label> element, satisfying the rule. The result JSONs now carry only
the intentional color-contrast fails — no fixture-artifact failures.
Dark fixtures use ThemeProvider without CssBaseline, so inherited-color
text rendered black on the dark background — a false contrast fail in
the probe. Adding `color: 'text.primary'` to the wrapping Box gives
inherited text the dark-mode foreground, matching real apps. This
removes false-positive dark-mode fails: Checkbox, Radio, Switch, Tabs
now pass cleanly. Also pulls in the CssBaseline addition in
BadgeColorContrastDark.
Move the 36 per-component ColorContrast fixtures from
fixtures/{Component}/{Component}ColorContrast{Light,Dark}.js to a flat
fixtures/ColorContrast/{Component}{Light,Dark}.js suite. Consequences:

- Routes become /regression-ColorContrast/{Component}{Light,Dark}.
- The 18 A11Y_RULES entries collapse to one glob
  (test/regressions/fixtures/ColorContrast/*).
- The 18 per-component results consolidate into a single
  test/regressions/a11y/results/ColorContrast.a11y.json, keyed by
  {Component}{Light,Dark}.
- 8 fixture dirs that only held ColorContrast files are removed.

Re-ran the suite: 36 demos, 15 with color-contrast fails, 0 non-cc
fails.
The reporter now derives the component (output file) and mode (entry
key) from the `{Component}{Light,Dark}` demo name, so
test/regressions/a11y/results/ holds one {Component}.a11y.json per
component again — keyed by `Light` / `Dark` — instead of a single
consolidated ColorContrast.a11y.json. Fixtures stay flat under
fixtures/ColorContrast/.
AppBar fills its bar with palette[color].main + contrastText (the same
failure pattern as Button contained) and is a major surface; Typography
renders text in palette[color].main on the default bg. Both pick up the
existing fixtures/ColorContrast/* glob — no demoMeta change.

AppBar dark needs enableColorOnDark (palette colors are off by default
in dark mode) and a MuiAppBar backgroundImage: 'none' override (it
extends Paper, whose dark-mode --Paper-overlay gradient trips axe's
bgGradient guard).

Results: AppBar fails info/warning (light) and error (dark); Typography
fails info/warning (light), passes dark.
Improves color contrast of the selected state (white-text-on-tint
combos failed WCAG AA): pick the dark shade in light mode and the
light shade in dark mode instead of `main`.
Interactive experiment at /experiments/color-contrast-tokens: tweak the
default palette tokens that fail the color-contrast regression, with live
axe-core pass/fail badges and current-vs-proposed previews per component.
Includes the accessibility gaps analysis doc.
@code-infra-dashboard
Copy link
Copy Markdown

code-infra-dashboard Bot commented May 12, 2026

Deploy preview

https://deploy-preview-48530--material-ui.netlify.app/

Bundle size

Bundle Parsed size Gzip size
@mui/material 🔺+124B(+0.02%) 🔺+32B(+0.02%)
@mui/lab 0B(0.00%) 0B(0.00%)
@mui/private-theming 0B(0.00%) 0B(0.00%)
@mui/system 0B(0.00%) 0B(0.00%)
@mui/utils 0B(0.00%) 0B(0.00%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

@siriwatknp siriwatknp marked this pull request as draft May 12, 2026 10:02
@siriwatknp siriwatknp changed the title [test] Add color-contrast a11y regression suite + token tuning experiment [POC] Add color-contrast a11y regression suite + token tuning experiment May 12, 2026
Open with the live defaults (clean URL) instead of the pre-filled
proposal; 'Load proposal' still applies the suggested tweak.
@oliviertassinari
Copy link
Copy Markdown
Member

oliviertassinari commented May 12, 2026

We fixed several contrast issues in the past. The ones I remember were about the dark/light grey, e.g., #9407, #25046.

Now, when it comes to colors, I wonder. For example, the alert, we have #46319, that we were aware of in the initial design of the component #18702 (comment). It was part of the design tradeoff, I believe.

At the end of the day, isn't it more important for the default look to feel great than to pass some contrast threshold? For example, https://www.radix-ui.com/themes/playground has a fair amount of contrast issues, and the UX is fine?
So I wonder about the room we have, is it possible to improve things? (from an UX standpoint and from a breaking change standpoint).

@siriwatknp
Copy link
Copy Markdown
Member Author

Need to account for #44179 too

@siriwatknp
Copy link
Copy Markdown
Member Author

At the end of the day, isn't it more important for the default look to feel great than to pass some contrast threshold? For example, https://www.radix-ui.com/themes/playground has a fair amount of contrast issues, and the UX is fine?
So I wonder about the room we have, is it possible to improve things? (from an UX standpoint and from a breaking change standpoint).

I am from a different perspective. My goal is to make the default passed the WCAG AA with minimal changes.
To me, UX is a different story and I don't want to mix it with this PR yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants