[poc] Material UI base vs md theme#48396
Draft
mnajdova wants to merge 110 commits into
Draft
Conversation
…/mui-css-layer
…i into feat/mui-css-layer
…/mui-css-layer
…/mui-css-layer
…/mui-css-layer
…/mui-css-layer
# Conflicts: # .codesandbox/ci.json # babel.config.js # docs/data/material/customization/css-layers/CssLayersInput.js # docs/data/material/customization/css-layers/CssLayersInput.tsx # docs/data/material/customization/css-layers/css-layers.md # docs/next.config.ts # docs/pages/material-ui/customization/css-layers.js # packages/mui-material-nextjs/src/v13-pagesRouter/createCache.ts # packages/mui-material/src/Slider/Slider.js # packages/mui-material/src/styles/createThemeNoVars.d.ts # packages/mui-system/src/GlobalStyles/GlobalStyles.tsx # packages/mui-system/src/ThemeProvider/useLayerOrder.test.tsx # packages/mui-system/src/ThemeProvider/useLayerOrder.tsx # packages/mui-system/src/createStyled/createStyled.js # packages/mui-system/src/styleFunctionSx/styleFunctionSx.js # packages/mui-system/src/styleFunctionSx/styleFunctionSx.test.js # scripts/build.mjs # scripts/copyFilesUtils.mjs
Deploy preview
Bundle size
Check out the code infra dashboard for more information about this PR. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Multi-Theme Plain CSS: Architectural Analysis
This document covers the two structural challenges that emerge when scaling a plain-CSS
theming system (base theme + MD opt-in) across a full component library: bundling
granularity and token proliferation. Each section describes the problem, the
available options, and the honest trade-offs of each.
Problem 1: Bundling — Loading only what you need
The constraint
CSS has no runtime tree-shaking. A stylesheet is either loaded or it isn't. There is no
mechanism to include only the rules that correspond to components actually rendered on the
page, or only the rules that belong to the active theme. This is a fundamental difference
from JavaScript, where bundlers eliminate unused exports at build time.
Options
A — Single theme bundle
material-theme.cssimports every component's MD overrides in one file:Users import one file; all MD component styles are present regardless of which
components are actually used on the page.
Realistic size: ~30–80 KB uncompressed, ~8–15 KB gzipped for a complete MD component
override set. Acceptable for most production apps.
B — Component JS import as the tree-shaking unit
Each component imports its own MD CSS alongside its base CSS:
material-theme.csscontains only token variables. The JS bundler (webpack, Vite)eliminates unused component imports. Pages that never render
<Slider>never loadSlider.material.css.This is the best fit for MUI's component-centric mental model. The inert MD bytes
per component are small and compress well; for most apps the trade-off is acceptable.
C — Manual per-theme imports
Users import exactly the files they need:
Only viable as an advanced escape hatch, not the default.
D — CSS-in-JS for theme structural overrides
Base component styles stay in
.cssfiles (tree-shaken by component JS import).Theme structural overrides are expressed in
styled()orsx— injected into theDOM only when the component renders. This is how MUI v5 works today.
sx/styledto overrideE — Build-time CSS generation
Theme is configured at build time. A tool generates a single optimised CSS output
containing only the active theme's rules. No runtime switching possible.
Right answer for static sites and design systems with a fixed theme, not for MUI's
general-purpose use case.
F — Copy to project (shadcn/ui model)
MUI ships the CSS files as templates. Users copy
base-theme.css,material-theme.css, and any component CSS files they need directly into theirproject, then edit them freely. The library ships no runtime CSS — only the JS
components and the source files as reference.
Is it a good option? It depends on what is being copied.
For theme token files (
base-theme.css,material-theme.css) — yes, genuinelygood. Token declarations are stable, change infrequently, and the whole point of a
starting-point theme is that users will customise it. Encouraging users to copy and
own these files is reasonable and already aligns with how this PoC is structured.
For component CSS files (
Slider.css,Button.css) — much riskier. These filesreference class names that MUI generates internally. A patch release that renames
.MuiSlider-thumbto.MuiSlider-handle(unlikely, but possible) silently breaksevery copied file. The more components are copied, the higher the ongoing maintenance
tax.
The practical sweet spot is a hybrid: keep component CSS managed by MUI (delivered
via the npm package, updated with each release), and encourage users to copy only the
theme token files as a starting point for customisation. This is distinct from shadcn/ui,
where the components themselves are intentionally decoupled from any upstream — MUI's
value proposition is precisely the maintained, accessible component implementation.
Bundling recommendation
Option B (component JS import as the tree-shaking unit) is the best default.
The component import is already the unit of granularity users think in. The dormant
MD bytes for base-theme users are minimal per-component and compress well.
Option F (copy to project) is the right answer specifically for theme token files —
ship
base-theme.cssandmaterial-theme.cssas well-commented starting points thatusers are explicitly invited to copy and modify. For component CSS, keep it managed.
For apps where bundle cost is the primary concern, Option E (build-time) is the
correct answer — but it is a different product, not a configuration of the same one.
Problem 2: Token Proliferation
The constraint
Every CSS custom property you expose is a public API. It must be documented,
kept stable across versions, and maintained when the underlying CSS changes.
Tokens also interact — setting
--Slider-thumbSizeand--Slider-trackSizeindependently can produce visually incoherent combinations. There is a real cost
to each token added, and it compounds across components.
The Slider PoC already has 12 component-level tokens. MUI has 50+ components.
A naive extrapolation — 12 × 50 — yields 600+ component tokens on top of ~100
global tokens. At that scale, discoverability collapses.
Token tiers
Level 1 — Global tokens (~50–100 variables)
--mui-palette-primary-main,--mui-shape-borderRadius,--mui-shadows-2Brand-level changes. High leverage: one change propagates to every component that
references that token. Well-understood by developers today.
Level 2 — Component tokens (~5–15 per component)
--Slider-thumbSize,--Slider-trackSize,--Slider-thumbElevationThe component's visual "knobs" — values that themes intentionally set differently,
and that users commonly need to override in design system work. One token maps to
one clear visual property.
Level 3 — Sub-part tokens (unbounded)
--SliderTrack-borderTopLeftRadius,--SliderThumb-boxShadowSpreadFull CSS property access for every internal element. Maximum flexibility,
maximum API surface.
Options
A — Global tokens only
No component-level tokens. All component customisation via CSS class overrides.
Not sufficient for a two-theme system. Themes need to set component-level values
differently without the user writing CSS.
B — Global + essential component tokens (current approach)
~10 tokens per component, chosen to cover the meaningful visual differences between
themes and the most common user customisation needs. Not every CSS property gets a
token — only the ones that themes actually differ on.
A token earns its place when:
Avoid tokens for values that are derived from other tokens. For example,
--Slider-hoverRingColoris alwaysrgba(var(--mui-palette-primary-mainChannel) / 0.16)—that relationship belongs in CSS, not duplicated as a configurable token. Surfacing it
as a token implies it's independently useful when in practice it almost never is.
Suggested cap: 10–15 tokens per component. If you need more to express the
difference between two themes, that is a signal those themes are structurally different
enough to warrant scoped CSS rules (see Problem 1, Option B) rather than more tokens.
C — Full sub-part tokens
Every CSS property on every internal element gets a token.
This is the direction tools like Fluent UI Web Components and SAP Fiori take.
It works when the component library is the design system — one team, one product.
It does not work well for a general-purpose library serving thousands of diverse
design systems.
D — Tiered exposure with a layer escape hatch
Start with global + component tokens (Option B). For sub-part customisation, instead
of adding more tokens, rely on
@layerto give user styles guaranteed priority:Users who need to change something not covered by a token write a standard CSS
override — they never need to hunt for an obscure token name or file a GitHub issue.
@layerspecificityE — Copy to project, no tokens needed
When users own the theme CSS files, they do not need a token API. They change the
value directly in the file. This sidesteps the entire token proliferation problem
for the theme layer.
--Slider-thumbElevationcommunicates intent; a rawbox-shadowvalue does notThis reinforces the hybrid conclusion from the Bundling section: copy-to-project is
appropriate for theme files, not for component files. Component-level tokens remain
necessary for users who want to customise without writing class-name CSS.
What tokens cannot express
Some visual differences between themes require structural CSS — different properties,
different pseudo-elements, different nesting. No number of tokens can encode these:
These require
[data-theme]-scoped CSS rules in a higher@layer, not more tokens.This is the real boundary to establish: tokens for values, scoped CSS rules for
structure.
Token recommendation
Option B (global + ~10 essential component tokens) plus Option D (
@layerasthe structural escape hatch).
Concrete decision rule:
Summary
The recommendations compose naturally:
base-theme.css/material-theme.cssdirectly. Treated as a well-commented starting point, not a versioned API.@layeras escape hatch). Component tokens remain necessary even when theme files are copied, because users should not need to know MUI's internal class names to adjust a slider's track size.Created with multiple iterations with Claude.
The PR is built on top of #44407. Only b52fccd is relevant for this POC.