[POC] Add color-contrast a11y regression suite + token tuning experiment#48530
[POC] Add color-contrast a11y regression suite + token tuning experiment#48530siriwatknp wants to merge 15 commits into
Conversation
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.
Deploy previewhttps://deploy-preview-48530--material-ui.netlify.app/ Bundle size
Check out the code infra dashboard for more information about this PR. |
Open with the live defaults (clean URL) instead of the pre-filled proposal; 'Load proposal' still applies the suggested tweak.
|
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? |
|
Need to account for #44179 too |
I am from a different perspective. My goal is to make the default passed the WCAG AA with minimal changes. |
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
colorprop.maintoken is not creating enough contrastToggleButtonandPaginationItemselected color need adjustment, tweaking only themaintoken is not enough because it has translucent background when selected.Changes
4.5:1ToggleButtonandPaginationItemto be