Skip to content

feat(theme): Add medium and high contrast theme - #5100

Open
hristototov wants to merge 2 commits into
mainfrom
hristototov/medium-and-high-contrast-theme
Open

feat(theme): Add medium and high contrast theme#5100
hristototov wants to merge 2 commits into
mainfrom
hristototov/medium-and-high-contrast-theme

Conversation

@hristototov

@hristototov hristototov commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Motivation

The theme only generates the MD3 standard contrast level. MD3 defines three levels, and the higher two exist for accessibility: they raise the contrast between text and background so the UI stays readable for users with low vision and in bright light. Without them, apps built with Paper have no supported way to meet the raised contrast ratios.

This adds the medium and high schemes for light and dark, and a contrast prop on PaperProvider to select them. standard stays the default and its colours do not change. It uses scripts/generate-contrast-tokens.ts

Measured against the baseline #6750A4 seed:

scheme primary tone onPrimaryContainer tone
light / standard 40.1 10.1
light / medium 26.7 100.0
light / high 13.3 86.9
dark / high 96.7 23.8

API

<PaperProvider contrast="high">
  <App />
</PaperProvider>

Related issue

Closes #5092

Test plan

Automated:

  • src/theme/__tests__/contrast.test.ts checks 17 text/background role pairs across both modes and both new levels, 64 pairs in total, and asserts >= 4.5 for medium and >= 7.0 for high. It also asserts that LightTheme and DarkTheme colours are unchanged.
  • src/core/__tests__/PaperProvider.test.tsx covers the contrast prop, the standard default, prop precedence over theme.contrast, and that a provider with only contrast set still follows the system color scheme.
  • No component snapshot colours changed. The only snapshot diff is the new contrast field on the serialized theme.

Manual, on an Android 12 emulator (API 31), using the new Contrast control in the example app drawer under Preferences:

  1. Open the drawer, switch between Standard, Medium and High. Colours change at each step.
  2. Toggle Dark Theme and repeat. All six combinations render correctly with no missing colours.
  3. With "Use Dynamic Theme" on, note that Standard shows the wallpaper-based colours while Medium and High fall back to the contrast-correct scheme. This is the intended behaviour described above.
  4. Restart the app. The selected level is restored from storage.
android-contrast.mov
ios-contrast.mov

@hristototov hristototov self-assigned this Sep 3, 2026
@hristototov hristototov added the v6 label Sep 3, 2026
@hristototov
hristototov requested review from adrcotfas and satya164 and removed request for satya164 September 3, 2026 11:38
Comment on lines +14 to +15
/** Only `standard` uses the reference palette steps. The other levels need
* tones that are not in the palette, so they live in `./contrastSchemes`. */

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These don't match MD3's published contrast schemes. On the color roles page, light/high primary is md.ref.palette.primary20 (#381E72), but this ships #312259; secondary, outline, outline-variant and on-surface-variant are off too, and surface-container isn't meant to change with contrast at all. Every tone MD3 asks for is already a key of ref/palette.ts - could roleToTone just gain medium and high maps, the way the old // extend with 'medium' | 'high' when those ship comment planned?

return {
...base,
...props.theme,
contrast: level,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing a theme that has its own colors silently overrides the contrast scheme, but theme.contrast still reports the level you asked for - so useTheme().contrast says "high" while the colours on screen are standard. Worth reconciling, given the docs tell people to read the level back off the theme.

Comment thread example/src/index.tsx
? DarkTheme
: LightTheme;
? getDynamicTheme(isDarkMode, contrast)
: createTheme({ dark: isDarkMode, contrast });

@JKobrynski JKobrynski Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This builds a new theme object on every render, so the provider rebuilds the theme and every themed component re-renders with it. getTheme(isDarkMode, contrast) returns the cached one worth using that here?

@JKobrynski

Copy link
Copy Markdown
Collaborator

And we've got conflicts

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add MD3 medium and high contrast levels

2 participants