Skip to content

Move the colour engine to material-theme-builder - #572

Merged
abernier merged 3 commits into
mainfrom
mtb
Aug 18, 2026
Merged

Move the colour engine to material-theme-builder#572
abernier merged 3 commits into
mainfrom
mtb

Conversation

@abernier

@abernier abernier commented Aug 18, 2026

Copy link
Copy Markdown
Member

Extracted from #571, which was doing this swap in passing while also moving the colour layer to pmndrs/design-system. Two separable things: which package computes the palette, and where the layer that maps it lives. This is the first. #571 now sits on top of it and is the second alone.

react-mcu gives way to material-theme-builder, its successor — and the package the design system's MD3 registry item installs, which is why it has to happen either way.

At the API level, it is a rename

--mcu-* becomes --md-sys-color-*, the standard MD3 system-token name. <Mcu> becomes <Mtb> and takes the same props.

The --color-* names the @theme mapping declares are identical either way, so every bg-surface, bg-primary-container and text-on-surface-variant in the codebase keeps working untouched. The THEME_* env vars — seed, scheme, contrast and the five alert colours — behave exactly as before.

Two components name the raw variables rather than a Tailwind utility: Code for its fixed prism colour, Sandpack for its three surface levels. Those four lines are the only component edits.

The palette is not identical, though

blend: true harmonizes the five alert colours against the seed, and the two packages harmonize differently. Rendered both against this repo's config and resolved to hex: 57 of the 67 roles match exactly, including all 49 standard M3 ones. The 10 that differ all belong to the custom colours.

role react-mcu material-theme-builder
note #a0caff #a1c9fd
note-container #00497e #1b4975
tip #71dba5 #90d5ae
on-tip-container #8ef7c0 #abf2c9
on-important #1f00a5 #2c2960
important-container #372ac0 #424078
warning #e5c446 #dec56e
on-warning-container #ffe174 #fbe186
on-caution #670024 #561d29
caution-container #900036 #72333f

The pattern: material-theme-builder pulls a custom colour further toward the seed the further its hue sits from it. note is already near the slate's blue and barely moves; important (violet) and caution (red) move a lot.

So the markdown alerts get more muted backgrounds — visibly for Important and Caution. Tip and Warning keep their container colour; only their foreground roles shift, which nothing renders.

Chromatic build 292 shows exactly this: 12 changes over the six pages that render an alert, and in the diff only the Important and Caution panels are flagged.

This is the one visible change in this PR — worth a decision rather than an accept-all, since both packages are ours. If react-mcu's harmonization was the intended one, the fix belongs upstream in material-theme-builder, not here.

It also stops restating the layer

globals.css was hand-writing two blocks the package already ships. Both are now one directive each:

@import 'material-theme-builder/shadcn.css';
@plugin 'material-theme-builder/tailwind' {
  custom-colors: note, tip, important, warning, caution;
}

The plugin covers all 67 names the @theme mapping declared — diffed programmatically, zero missing — and adds surface-tint, surface-variant and the ref-palette shades. It also takes the custom colour names, which a static stylesheet could not: each of the five expands to its four roles, so the twenty hand-written alert lines go too — and with them the failure they invited, where a name declared in the config and forgotten in the mapping emitted no rule and no error.

The shadcn remap was byte-identical to the package's shadcn.css, so that import is pure dedup. Its selectors are doubled — :root:root, .dark.dark — so it outranks shadcn's own blocks by specificity rather than by source order, which is why it sits up with the other imports. --radius is untouched: shadcn.css defines no non-colour.

globals.css loses 95 lines and every --md-sys-color- reference.

Note

v5.0.0 removed material-theme-builder/tailwind.css; it no longer resolves. The @plugin form is the replacement and the only way to map custom colours.

Verified

  • The emitted CSS is rule-for-rule identical before and after, bar one indirection. Built both states and diffed the rule sets: 493 rules each side, 38 differ, and every one of the 38 is the same selector and property with var(--color-surface-dim) become var(--md-sys-color-surface-dim). The @layer theme block loses exactly the 18 --color-* aliases and nothing else.
  • Checked in the running build, since a missing utility is exactly the failure a build stays quiet about. Across the second commit — hand-written blocks versus the two directives — the five alerts compute byte-identical backgrounds in both themes — dark rgb(27,73,117) / rgb(0,82,52) / rgb(66,64,120) / rgb(85,69,0) / rgb(114,51,63), light rgb(210,228,255) / rgb(171,242,201) / rgb(227,223,255) / rgb(251,225,134) / rgb(255,217,221). shadcn's --background, --primary, --border and --ring all resolve to their M3 roles rather than shadcn's stock oklch, confirming the doubled selector wins.
  • The palette diff above is measured, not eyeballed: both packages rendered against this repo's exact config (slate seed, tonalSpot, contrast 0, the five blended alerts), --md-sys-color-* resolved through --md-ref-palette-* to hex, then compared name by name.
  • Full next build green, and the built HTML carries 69 --md-sys-color-* variables server-rendered, with no --mcu-* left anywhere in the output.
  • tsc clean, prettier --check clean, 95 tests green.
  • lint reports two react/no-unescaped-entities in src/stories/Page.tsx; both pre-date this branch, verified against main.

🤖 Generated with Claude Code

https://claude.ai/code/session_018NUhr59QsGjuq2iACFhxX8

abernier and others added 3 commits August 18, 2026 19:14
react-mcu gives way to material-theme-builder, its successor and the
package pmndrs/design-system's MD3 registry item installs. Doing the swap
on its own leaves that item as a pure question of where the colour layer
lives, rather than which package computes it.

At the API level it is a rename: --mcu-* becomes --md-sys-color-*, the
standard MD3 system-token name. <Mcu> becomes <Mtb> and takes the same
props. The --color-* names the @theme mapping declares are identical either
way, so every bg-surface, bg-primary-container and text-on-surface-variant
keeps working untouched, and the THEME_* env vars behave as before.

Two components name the raw variables rather than a Tailwind utility --
Code for its fixed prism colour, Sandpack for its three surface levels --
and are the only component edits.

The palette is NOT identical, though. Rendering both packages against this
repo's config and resolving to hex: 57 of the 67 roles match exactly,
including all 49 standard M3 ones. The 10 that differ all belong to the
five `blend: true` custom colours, and the two harmonize differently --
material-theme-builder pulls a custom colour further toward the seed the
further its hue sits from it.

  note                #a0caff -> #a1c9fd
  note-container      #00497e -> #1b4975
  tip                 #71dba5 -> #90d5ae
  on-tip-container    #8ef7c0 -> #abf2c9
  on-important        #1f00a5 -> #2c2960
  important-container #372ac0 -> #424078
  warning             #e5c446 -> #dec56e
  on-warning-container#ffe174 -> #fbe186
  on-caution          #670024 -> #561d29
  caution-container   #900036 -> #72333f

So the markdown alerts get more muted backgrounds -- most visibly Important
and Caution, whose hues are furthest from the slate seed; Tip and Warning
keep their container colour. Chromatic build 292 shows exactly this, on the
six pages that render an alert.

Verified: full build green, 69 --md-sys-color-* variables server-rendered
and no --mcu-* left anywhere. tsc, prettier and the 95 tests clean; lint
reports two pre-existing react/no-unescaped-entities in src/stories/Page.tsx,
verified against main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NUhr59QsGjuq2iACFhxX8
globals.css restated, by hand, two blocks the package already ships: a
~60-line @theme mapping of --color-* onto --md-sys-color-*, and a 31-line
:root/.dark remap of shadcn's variables onto M3 roles. Both are now one
directive each.

  @import 'material-theme-builder/shadcn.css';
  @plugin 'material-theme-builder/tailwind' {
    custom-colors: note, tip, important, warning, caution;
  }

The plugin covers all 67 names the mapping declared -- diffed
programmatically, zero missing -- and adds surface-tint, surface-variant
and the ref-palette shades. It also takes the custom colour names, which a
static stylesheet could not: each of the five expands to its four roles, so
the twenty hand-written alert lines go too, and with them the failure they
invited. A name declared in the config and forgotten in the mapping emitted
no rule and no error.

The shadcn remap was byte-identical to the package's shadcn.css, so the
import is pure dedup. Its selectors are doubled -- :root:root, .dark.dark --
so it outranks shadcn's own blocks by specificity rather than by source
order, which is why it can sit up with the other imports.

Net: globals.css loses 95 lines and every --md-sys-color- reference.

Verified. The emitted CSS is rule-for-rule identical before and after, bar
one indirection: each of the 38 differing rules is the same selector and
property with var(--color-surface-dim) become var(--md-sys-color-surface-dim).
The @layer theme block loses exactly the 18 --color-* aliases and nothing
else. In the running build the five alerts compute byte-identical
backgrounds in both themes -- dark rgb(27,73,117) / rgb(0,82,52) /
rgb(66,64,120) / rgb(85,69,0) / rgb(114,51,63), light rgb(210,228,255) /
rgb(171,242,201) / rgb(227,223,255) / rgb(251,225,134) / rgb(255,217,221) --
and shadcn's --background, --primary, --border and --ring all resolve to
their M3 roles rather than shadcn's stock oklch, confirming the doubled
selector wins. --radius is untouched: shadcn.css defines no non-colour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NUhr59QsGjuq2iACFhxX8
The `:root` and `.dark` blocks shadcn vendors are still in the file, and
every colour in them is overridden by the `shadcn.css` import above --
doubled selectors, so it wins wherever it sits. Only `--radius` is live;
that import defines no non-colour.

They stay verbatim rather than get trimmed: it is what the CLI writes, and
hand-editing it would come back on every future run. But a reader had no way
to tell the live values from the dead ones, so the block now says which it is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NUhr59QsGjuq2iACFhxX8
@abernier
abernier merged commit 96c442f into main Aug 18, 2026
9 checks passed
@github-actions github-actions Bot mentioned this pull request Aug 18, 2026
@abernier
abernier deleted the mtb branch August 18, 2026 17:28
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.

1 participant