Skip to content

[POC] Use ThemeCSSProvider as replacement for ThemeProvider#48652

Draft
silviuaavram wants to merge 40 commits into
mui:masterfrom
silviuaavram:poc/css-theme-provider
Draft

[POC] Use ThemeCSSProvider as replacement for ThemeProvider#48652
silviuaavram wants to merge 40 commits into
mui:masterfrom
silviuaavram:poc/css-theme-provider

Conversation

@silviuaavram

@silviuaavram silviuaavram commented Jun 10, 2026

Copy link
Copy Markdown
Member

This PR aims to act as a POC for using a CSSThemeProvider instead of ThemeProvider, which should not import anything Emotion related. To be used by consumers who want to replace Emotion styling with anything else: Tailwind, CSS Modules etc. It's one of the ways we can remove Emotion and keep the Theme generation at runtime. The other way is to generate the theme at build time and import the file. This PR will cover just the first option. The other PR is here.

Changes:

Summary

Component CSS

  • Move the base styles for Slider and Toolbar from styled() arguments to CSS Modules.
    • Keep their styled() slot shells so Emotion users retain sx, styled(), variants, and styleOverrides.
    • Use :global(.Mui*) selectors to preserve MUI's stable public class names.
    • Author Toolbar media queries using aliases such as @media (--mui-breakpoint-up-sm).

CSS Delivery

Provide two mutually exclusive component CSS paths:

  1. Default breakpoints:

    • buildCssArtifacts.mts combines component CSS Modules into @mui/material/styles.css.
    • Lightning CSS resolves the aliases with MUI's default breakpoint values.
    • Consumers import @mui/material/styles.css once.
    • This path requires no custom CSS processing, but includes all migrated component styles.
  2. Custom breakpoints:

    • buildCssModulesVariant.mts creates a parallel @mui/material/css-modules/* package tree.
    • Components in this tree import their corresponding CSS Modules.
    • Consumers configure PostCSS or Lightning CSS to resolve aliases from their theme.
    • muiMaterialCssModules() maps normal Material UI imports to the parallel package.
    • Consumers must not also import @mui/material/styles.css, as that would duplicate styles.

CSS Build Tools

  • Add @mui/material-css-tools for build-time integrations.
  • Provide muiMaterialCssModules() to select the parallel package in Vite.
  • Provide muiCustomMedia({ theme }) adapters for Vite/Lightning CSS and PostCSS.
  • Provide generateBreakpointCustomMedia() and
    getBreakpointCustomMediaDefinitions() for custom bundler integrations.
  • Application CSS can use aliases such as @media (--mui-breakpoint-up-tablet).
  • Breakpoints are resolved once for the entire CSS build. Nested themes cannot use different
    breakpoint values.

CssThemeProvider

  • Add CssThemeProvider as a runtime replacement for ThemeProvider on the no-Emotion path.
  • Require a theme created with createTheme({ cssVariables: true }).
  • Inject theme variables through CssVarsInjector.
    • On the server, it renders a <style> element into the HTML stream.
    • On the client, useInsertionEffect inserts or updates the style in <head>.
  • Provide the resolved JavaScript theme through @mui/private-theming, allowing useTheme()
    and default props to work.
  • Preserve direction through RtlProvider.
  • Support root theme switching and scoped nested themes.
  • Theme callbacks are currently rejected; consumers must pass a theme object.
  • sx, styled(), theme variants, and styleOverrides do not generate CSS when the noop
    styling engine is active. sx emits a development warning and is ignored.

Color Scheme

  • Add useCssColorScheme() for trees managed by CssThemeProvider.
  • Extract the shared implementation into useColorSchemeSetup, which is used by both
    CssVarsProvider and CssThemeProvider.
  • Support mode persistence, system preference, cross-tab synchronization, and updating the
    color-scheme attribute.
  • Nested providers reuse the outer provider's active color scheme.
  • noSsr currently affects color-scheme initialization; it does not suppress server rendering
    or render a placeholder as its prop documentation suggests.

Theme Scoping

  • Use ThemeScope as the DOM boundary for nested CSS-variable themes.
  • getThemeScopeProps() derives the scope class and color-scheme attribute from simple
    class-based rootSelector values.
  • Both ThemeProvider and CssThemeProvider automatically render it for nested themes.
  • ThemeScopeContext exposes the active scope to portal components.
  • useThemeScopeProps() copies the scope onto a portal root.
  • The PoC ScopedDialog demonstrates the integration through slotProps.root.
  • A production implementation should integrate this into Modal or a shared portal container.
  • Multiple simultaneous nested themes need distinct selectors or explicit styleId values so
    their injected styles do not overwrite one another.

No-Emotion Engine

  • Add @mui/styled-engine-noop.
  • Alias @mui/styled-engine to the noop package in both CssThemeProvider sandboxes.
  • The package preserves component structure and prop forwarding without injecting CSS-in-JS.
  • Plain CSS, CSS Modules, Tailwind CSS, and inline styles remain available.

Tailwind CSS

  • Add @mui/tailwind with Tailwind v3 preset/plugin and Tailwind v4 CSS integration.
  • Add theme-specific helpers to @mui/material-css-tools:
    • createTailwindPreset(theme) for Tailwind v3.
    • generateTailwindThemeCss(theme) for Tailwind v4.
  • Tailwind palette tokens reference MUI CSS variables, so they follow runtime and nested themes.
  • Tailwind and component breakpoints are generated from the same build-time theme.
  • The docs import @mui/tailwind/v4.css without requiring postcss-import.

Sandboxes

  • emotion-vite-sandbox: Emotion, runtime ThemeProvider, CSS Modules, PostCSS,
    custom sm=720px, and Tailwind v3.
  • css-theme-provider-vite-sandbox: noop engine, runtime CssThemeProvider, CSS Modules,
    Lightning CSS, custom sm=720px, and Tailwind v4.
  • css-theme-provider-default-css-vite-sandbox: noop engine, runtime CssThemeProvider,
    aggregate @mui/material/styles.css, default sm=600px, and Tailwind v4.
  • The sandboxes exercise Slider, Toolbar, theme switching, nested themes, portal scoping,
    useTheme(), useCssColorScheme(), plain CSS overrides, ignored sx, and Tailwind tokens.
  • They do not currently contain a styleOverrides example.
  • Their buildMuiCss.mts scripts generate Tailwind files only. Component CSS and breakpoint
    processing are handled by the selected CSS delivery path.

Docs Experiments

  • Add /experiments/emotion-slider/ and /experiments/css-slider/.
  • These are provider API sketches rather than reliable no-Emotion tests because the docs use
    the Emotion styling engine.
  • They must also import the aggregate stylesheet or select the CSS Modules package before they
    accurately test the migrated component base styles.

Verification

pnpm -F @mui/material build
pnpm -F @mui/material-css-tools build

pnpm -F @mui-internal/emotion-vite-sandbox dev
pnpm -F @mui-internal/css-theme-provider-vite-sandbox dev
pnpm -F @mui-internal/css-theme-provider-default-css-vite-sandbox dev

pnpm -F @mui-internal/emotion-vite-sandbox build
pnpm -F @mui-internal/css-theme-provider-vite-sandbox build
pnpm -F @mui-internal/css-theme-provider-default-css-vite-sandbox build

Copilot AI review requested due to automatic review settings June 10, 2026 12:54
@code-infra-dashboard

code-infra-dashboard Bot commented Jun 10, 2026

Copy link
Copy Markdown

Deploy preview

Bundle size

Bundle Parsed size Gzip size
@mui/material ▼-696B(-0.13%) 🔺+331B(+0.22%)
@mui/lab 🔺+44B(+0.13%) 🔺+12B(+0.14%)
@mui/private-theming 0B(0.00%) 0B(0.00%)
@mui/system 🔺+1.51KB(+2.17%) 🔺+429B(+1.75%)
@mui/utils 🔺+42B(+0.27%) 🔺+13B(+0.22%)

Details of bundle changes


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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a new @mui/tailwind package as a POC integration layer between MUI’s CSS variables (from CssVarsProvider) and Tailwind (v3 preset + v4 CSS directives), and wires it into the docs with a new integration page and demos.

Changes:

  • Introduce @mui/tailwind (Tailwind v3 preset + state-variant plugin + Tailwind v4 v4.css token mapping).
  • Update docs to consume @mui/tailwind/v4.css and add a new “Tailwind components” integration page + demos.
  • Adjust repo config/deps to include the new workspace package and remove postcss-import from docs.

Reviewed changes

Copilot reviewed 18 out of 20 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tsconfig.json Adds TS path aliases for the new @mui/tailwind package.
pnpm-lock.yaml Adds workspace link for @mui/tailwind and updates lock entries (incl. removal of postcss-import).
packages/mui-tailwind/src/v4.css Tailwind v4 CSS integration: breakpoints, token mapping, utilities, and mui-* variants.
packages/mui-tailwind/src/preset.js Tailwind v3 preset exposing MUI token mappings + plugin registration.
packages/mui-tailwind/src/plugin.js Tailwind plugin adding mui-{state} / mui-not-{state} variants.
packages/mui-tailwind/README.md Links to the docs integration page for setup and demos.
packages/mui-tailwind/package.json Declares the new package metadata and entrypoint exports.
docs/postcss.config.js Removes postcss-import from the docs PostCSS pipeline.
docs/pages/material-ui/integrations/tailwindcss/tailwindcss-components.js Adds a new docs route for the Tailwind components page.
docs/pages/global.css Imports @mui/tailwind/v4.css and updates layer ordering for Tailwind/MUI.
docs/package.json Adds @mui/tailwind workspace dependency; removes postcss-import.
docs/data/material/integrations/tailwindcss/TailwindFilterChips.tsx New TS demo using mui-selected: Tailwind variant on MUI ToggleButtons.
docs/data/material/integrations/tailwindcss/TailwindFilterChips.js JS version of the filter chips demo.
docs/data/material/integrations/tailwindcss/TailwindDisabledState.tsx New TS demo using mui-disabled: variant with Slider/Switch.
docs/data/material/integrations/tailwindcss/TailwindDisabledState.js JS version of the disabled-state demo.
docs/data/material/integrations/tailwindcss/tailwindcss-v4.md Points readers to the new @mui/tailwind package page.
docs/data/material/integrations/tailwindcss/tailwindcss-components.md New docs page describing @mui/tailwind setup (v3 + v4) and demos.
docs/data/material/integrations/tailwindcss/TailwindCard.tsx.preview Preview snippet for the Tailwind job card demo.
docs/data/material/integrations/tailwindcss/TailwindCard.tsx New TS Tailwind-styled job card demo built from MUI components.
docs/data/material/integrations/tailwindcss/TailwindCard.js JS version of the Tailwind job card demo (+ PropTypes).
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/mui-tailwind/src/preset.js
Comment thread packages/mui-tailwind/src/v4.css
Comment thread packages/mui-tailwind/package.json
@silviuaavram
silviuaavram force-pushed the poc/css-theme-provider branch from 39e2999 to 8b43394 Compare June 10, 2026 13:11
@silviuaavram silviuaavram added the scope: system The system, the design tokens / styling foundations used across components. eg. @mui/system with MUI label Jun 10, 2026
@silviuaavram
silviuaavram marked this pull request as draft June 10, 2026 13:11
@silviuaavram silviuaavram self-assigned this Jun 11, 2026
@silviuaavram
silviuaavram force-pushed the poc/css-theme-provider branch 2 times, most recently from 0c748fd to da7fa88 Compare June 12, 2026 08:34
@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jun 15, 2026
react(),
],
resolve: {
alias: [

@Janpot Janpot Jun 17, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You should remove these aliases and test against the build folders instead. These aliases tend to turn what's supposedly 3rd party code for bundlers into 1st party code. Which operates under different rules in some runtimes. In the past I've also masks module graph issues around csj/esm.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

sure, let me check.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yep, the testing will be a bit more complicated, as we would need to build the packages first, but at least we will test the real output of the libraries. @brijeshb42 I know you were creating something like this when we had pigment css apps, any suggestion for a specific setup?

@brijeshb42 brijeshb42 Jun 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As far as I can tell, we don't have any build-time integration here. Pigment needed to be integrated with the bundler, so I had to build with watch-mode (using tsup at the time) since we needed the transpiled code to be called in next.config.ts or vite.config.ts.
So the current setup should work whether using alias to source of build folder directly.
@mui/tailwind will need this setup of building in watch mode or manual builds to be used in docs.

@github-actions github-actions Bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jun 18, 2026
@silviuaavram
silviuaavram force-pushed the poc/css-theme-provider branch from e1a745e to f739cd0 Compare June 19, 2026 07:26
// This is exactly what non-Emotion users configure in their own bundler.
{
find: '@mui/styled-engine',
replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-styled-engine-noop/src'),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should keep this test as close to end-user environment as possible. So let's alias to the build folder

Suggested change
replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-styled-engine-noop/src'),
replacement: '@mui/styled-engine-noop',

and remove 'treat-js-files-as-jsx'.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

sure, let me do that!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I can't make this work. It only works with:

replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-styled-engine-noop/build'),

Using replacement: '@mui/styled-engine-noop', as you suggested gives this issue, and I was not able to solve it.

[plugin:vite:import-analysis] Failed to resolve import "@mui/styled-engine" from "../../packages/mui-system/build/index.mjs". Does the file exist?
/Users/silviuavram/Documents/git-repos/mui/material-ui/packages/mui-system/build/index.mjs:9:53
7  |   */
8  |  import _formatErrorMessage from "@mui/utils/formatMuiErrorMessage";
9  |  export { css, keyframes, StyledEngineProvider } from "@mui/styled-engine";
   |                                                        ^
10 |  export { default as GlobalStyles } from "./GlobalStyles/index.mjs";
11 |  export { default as borders } from "./borders/index.mjs";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Even when you ran pnpm release:build?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes. Maybe you can try as well, I can't get it to go.

[plugin:vite:import-analysis] Failed to resolve import "@mui/styled-engine" from "../../packages/mui-system/build/index.mjs". Does the file exist?
/Users/silviuavram/Documents/git-repos/mui/material-ui/packages/mui-system/build/index.mjs:9:53
7  |   */
8  |  import _formatErrorMessage from "@mui/utils/formatMuiErrorMessage";
9  |  export { css, keyframes, StyledEngineProvider } from "@mui/styled-engine";
   |                                                        ^
10 |  export { default as GlobalStyles } from "./GlobalStyles/index.mjs";
11 |  export { default as borders } from "./borders/index.mj

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah yes, the @mui/styled-engine-noop is not reachable from @mui/system as it's not a dependency, unlike @mui/styled-engine. This shouldn't be a problem on our user's end, but for use we could remediate by installing @mui/styled-engine-noop as a top-leveel dependency, then it becomes reachable from all packages

pnpm add @mui/styled-engine-noop@workspace:* -D -w

Not sure we should do this though. It's just as much not like end-user setup as the current situation is

@mnajdova

mnajdova commented Jun 22, 2026

Copy link
Copy Markdown
Member

Can you clarify this part:

For both examples, use sx, styleOverrides and style tag to change Slider. For css example in experiments, styleOverrides does not work, but sx does (no alias to style-noop). For css example in vite, sx and styleOverrides do not work.

What do you mean sx works in one case but not the other? Should sx not only non-emotion setup? Maybe replace "For css example in experiments" with "When using emotion (experiments)"

@mnajdova mnajdova left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ok maybe I need a explanation why we have mui.default? And the styles for Slider are under the mui.components layer?

Comment thread docs/data/material/integrations/tailwindcss/tailwindcss-components.md Outdated
Comment thread docs/pages/global.css Outdated
Comment thread packages/mui-tailwind/src/v4.css Outdated

@mnajdova mnajdova left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ok, so we have now the CSS vars provider that adds DOM element and ThemeScope provider to make the portaled element be able to read this value. This means that we will need to update all portaled elements to read this provider, no?

Comment thread packages/mui-material/src/Slider/Slider.module.css Outdated
const rtl = resolved.direction === 'rtl';

return (
<CssColorSchemeContext.Provider value={contextValue}>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we directly include here the ThemeScope if it is a nested theme?

@silviuaavram

Copy link
Copy Markdown
Member Author

Ok, so we have now the CSS vars provider that adds DOM element and ThemeScope provider to make the portaled element be able to read this value. This means that we will need to update all portaled elements to read this provider, no?

CSSThemeProvider aims to do what ThemeProvider does, through css variables: inject css vars, RTL, useTheme, useColorScheme (it's useCSSColorScheme since I useColorScheme is coupled in createCssVarsProvider.js), default props.

ThemeScope is the one that adds a wrapping DOM element to make both portaled and normal elements work inside a nested theme. The only difference between portaled and non-portaled in this case is the portaled elements subscribing to the ThemeScopeContext and getting the className, color scheme and color scheme attribute such that it adds them to their own root as well. Non-portaled will receive that wrapping div element, which already has the classname and color scheme attribute, and will receive the nested theme values for the variables.

@silviuaavram
silviuaavram force-pushed the poc/css-theme-provider branch from 4b582d6 to 32e8322 Compare June 25, 2026 06:55
@silviuaavram
silviuaavram force-pushed the poc/css-theme-provider branch from 0971d65 to 69c4173 Compare June 29, 2026 07:01

@mnajdova mnajdova left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't like that we have a "CSS in JS" implementation for the breakpoints, and moreover that we need to use a different file and JS syntax for defining the styles for the breakpoints.

Maybe the main benefit of the other approach where we built the CSS and provide users the content would be that the breakpoints can be an input and we can use some kind of "replace" method on them. Ideally, I would like to be able to do something like:

@media (min-width: $xs) {
  .header {/* some styles */}
}

and have the xs be replaced with the actual breakpoint. We are going here in the waters of zero-runtime CSS in JS, I think Pigment CSS already had something like this. cc @brijeshb42 may have more ideas on how we can solve this.

@Janpot

Janpot commented Jun 30, 2026

Copy link
Copy Markdown
Member

Maybe one option could be to export a version of our bundles, that import CSS modules, and that use @custom-media to define breakpoints. If a user configures this bundle, and makes sure their css preprocessor supports custom media queries, they can then override them for custom breakpoints.

edit: See @custom-media. The idea here is that we let users "polyfill" it through their CSS bundler.


// Components register at module import time; batch those imports into one update.
// Name-keying dedupes repeated imports/HMR for the same component.
export function registerBreakpointStyles(name: string, descriptor: BreakpointStylesDescriptor) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Have you tested this with server side rendering or RSC ?

@brijeshb42

Copy link
Copy Markdown
Contributor

There's no clear solution here in the absence of build-time extraction. Both @mnajdova and @Janpot's solution are on the same lines. Jan's is the standard one.

In our css modules, we'll need to use @media (--xs/sm/etc) everywhere and then ask the user to setup their own breakpoints in their css files and integrate postcss plugin - https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-custom-media or lightningcss depending on their bundlers.

@mnajdova

mnajdova commented Jul 1, 2026

Copy link
Copy Markdown
Member

Using a postcss plugin makes sense to me 👌 This is how mantine solved the problem - https://mantine.dev/styles/responsive/#breakpoints-variables-in-css-modules

@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 3, 2026
@github-actions github-actions Bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 3, 2026
@Janpot

Janpot commented Jul 3, 2026

Copy link
Copy Markdown
Member

... or lightningcss depending on their bundlers.

This is what attracts me to this solution, at least one widely used CSS pre-processor would work out-of-the box, no plugins required.

@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

PR: out-of-date The pull request has merge conflicts and can't be merged. scope: system The system, the design tokens / styling foundations used across components. eg. @mui/system with MUI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants