From 764dce74a2933745a06f27b7920013268d7d2cef Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Wed, 10 Jun 2026 16:10:53 +0300 Subject: [PATCH 01/21] packages/mui-tailwind --- packages/mui-tailwind/README.md | 6 + packages/mui-tailwind/package.json | 49 ++++++ packages/mui-tailwind/src/plugin.js | 34 ++++ packages/mui-tailwind/src/preset.js | 136 ++++++++++++++++ packages/mui-tailwind/src/v4.css | 232 ++++++++++++++++++++++++++++ pnpm-lock.yaml | 44 +++--- tsconfig.json | 2 + 7 files changed, 478 insertions(+), 25 deletions(-) create mode 100644 packages/mui-tailwind/README.md create mode 100644 packages/mui-tailwind/package.json create mode 100644 packages/mui-tailwind/src/plugin.js create mode 100644 packages/mui-tailwind/src/preset.js create mode 100644 packages/mui-tailwind/src/v4.css diff --git a/packages/mui-tailwind/README.md b/packages/mui-tailwind/README.md new file mode 100644 index 00000000000000..1b951636a3ad37 --- /dev/null +++ b/packages/mui-tailwind/README.md @@ -0,0 +1,6 @@ +# @mui/tailwind + +Tailwind CSS preset, plugin, and v4 CSS file for Material UI. + +Full documentation, setup guide, and demos are in +[`docs/data/material/integrations/tailwindcss/tailwindcss-components.md`](../../docs/data/material/integrations/tailwindcss/tailwindcss-components.md). diff --git a/packages/mui-tailwind/package.json b/packages/mui-tailwind/package.json new file mode 100644 index 00000000000000..6eb86825a21124 --- /dev/null +++ b/packages/mui-tailwind/package.json @@ -0,0 +1,49 @@ +{ + "name": "@mui/tailwind", + "version": "10.0.0-alpha.0", + "author": "MUI Team", + "description": "Tailwind CSS preset and plugin for Material UI.", + "license": "MIT", + "keywords": [ + "react", + "mui", + "tailwind", + "tailwindcss", + "preset", + "plugin" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/mui/material-ui.git", + "directory": "packages/mui-tailwind" + }, + "bugs": { + "url": "https://github.com/mui/material-ui/issues" + }, + "homepage": "https://mui.com/", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "sideEffects": false, + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0" + }, + "peerDependenciesMeta": { + "tailwindcss": { + "optional": true + } + }, + "exports": { + ".": "./src/preset.js", + "./preset": "./src/preset.js", + "./plugin": "./src/plugin.js", + "./v4.css": "./src/v4.css" + } +} diff --git a/packages/mui-tailwind/src/plugin.js b/packages/mui-tailwind/src/plugin.js new file mode 100644 index 00000000000000..f9aeced68a5f9d --- /dev/null +++ b/packages/mui-tailwind/src/plugin.js @@ -0,0 +1,34 @@ +const plugin = require('tailwindcss/plugin'); + +/** + * MUI state classes that components apply to themselves. + * Maps the variant name to the Mui- class that triggers it. + */ +const MUI_STATES = { + active: 'Mui-active', + checked: 'Mui-checked', + completed: 'Mui-completed', + disabled: 'Mui-disabled', + error: 'Mui-error', + expanded: 'Mui-expanded', + focused: 'Mui-focused', + 'focus-visible': 'Mui-focusVisible', + readonly: 'Mui-readOnly', + required: 'Mui-required', + selected: 'Mui-selected', +}; + +/** + * Tailwind plugin that adds `mui-{state}:` and `mui-not-{state}:` variants + * for every MUI component state class. + * + * Usage (Tailwind v3 or v4 via `@plugin`): + * + * + */ +module.exports = plugin(({ addVariant }) => { + Object.entries(MUI_STATES).forEach(([variant, muiClass]) => { + addVariant(`mui-${variant}`, `&.${muiClass}`); + addVariant(`mui-not-${variant}`, `&:not(.${muiClass})`); + }); +}); diff --git a/packages/mui-tailwind/src/preset.js b/packages/mui-tailwind/src/preset.js new file mode 100644 index 00000000000000..c8bcb71e73bb1d --- /dev/null +++ b/packages/mui-tailwind/src/preset.js @@ -0,0 +1,136 @@ +const muiPlugin = require('./plugin'); + +/** + * Helper: builds a palette color group that supports Tailwind's opacity modifiers + * (e.g. `bg-primary/50`) using MUI's pre-computed channel variables. + * + * MUI exposes `--mui-palette-{color}-mainChannel` as space-separated R G B so + * Tailwind can compose `rgb(R G B / )` at the utility level. + */ +function paletteColor(name) { + const v = (suffix) => `--mui-palette-${name}-${suffix}`; + return { + DEFAULT: `rgb(var(${v('mainChannel')}) / )`, + light: `rgb(var(${v('lightChannel')}) / )`, + dark: `rgb(var(${v('darkChannel')}) / )`, + contrast: `var(${v('contrastText')})`, + }; +} + +/** + * Tailwind v3 preset for Material UI. + * + * Usage in `tailwind.config.js`: + * module.exports = { presets: [require('@mui/tailwind')] }; + * + * What you get: + * - MUI palette colors as Tailwind colors (with opacity modifier support) + * - MUI breakpoints aligned to `sm/md/lg/xl` + * - MUI elevation shadows (`shadow-1` … `shadow-24`) + * - MUI border-radius token (`rounded-mui`) + * - MUI z-index scale + * - `mui-{state}:` and `mui-not-{state}:` variants + */ +module.exports = { + darkMode: ['class', '[data-mui-color-scheme="dark"]'], + + theme: { + // Override Tailwind's default screens to match MUI breakpoints. + // `xs` is intentionally omitted — it maps to 0px (the default / no breakpoint). + screens: { + sm: '600px', + md: '900px', + lg: '1200px', + xl: '1536px', + '2xl': '1920px', + }, + + extend: { + colors: { + // Semantic palette — opacity modifiers supported via channel vars + primary: paletteColor('primary'), + secondary: paletteColor('secondary'), + error: paletteColor('error'), + warning: paletteColor('warning'), + info: paletteColor('info'), + success: paletteColor('success'), + + // Text colors + // MUI text colors are pre-mixed rgba values; use var() directly. + // Opacity modifiers are not supported for these. + text: { + primary: 'var(--mui-palette-text-primary)', + secondary: 'var(--mui-palette-text-secondary)', + disabled: 'var(--mui-palette-text-disabled)', + icon: 'var(--mui-palette-text-icon)', + }, + + // Surface / background colors + background: { + default: 'var(--mui-palette-background-default)', + paper: 'var(--mui-palette-background-paper)', + }, + + // Divider + divider: 'var(--mui-palette-divider)', + + // Action state colors + action: { + active: 'var(--mui-palette-action-active)', + hover: 'var(--mui-palette-action-hover)', + selected: 'var(--mui-palette-action-selected)', + disabled: 'var(--mui-palette-action-disabled)', + 'disabled-bg': 'var(--mui-palette-action-disabledBackground)', + focus: 'var(--mui-palette-action-focus)', + }, + + // Material grey scale — used for neutral surfaces and borders + grey: { + 50: 'var(--mui-palette-grey-50)', + 100: 'var(--mui-palette-grey-100)', + 200: 'var(--mui-palette-grey-200)', + 300: 'var(--mui-palette-grey-300)', + 400: 'var(--mui-palette-grey-400)', + 500: 'var(--mui-palette-grey-500)', + 600: 'var(--mui-palette-grey-600)', + 700: 'var(--mui-palette-grey-700)', + 800: 'var(--mui-palette-grey-800)', + 900: 'var(--mui-palette-grey-900)', + A100: 'var(--mui-palette-grey-A100)', + A200: 'var(--mui-palette-grey-A200)', + A400: 'var(--mui-palette-grey-A400)', + A700: 'var(--mui-palette-grey-A700)', + }, + }, + + // MUI elevation shadows (0–24 matching Material Design levels) + boxShadow: Object.fromEntries( + Array.from({ length: 25 }, (_, i) => [i, `var(--mui-shadows-${i})`]), + ), + + // MUI shape token + borderRadius: { + mui: 'calc(var(--mui-shape-borderRadius) * 1px)', + }, + + // MUI z-index scale + zIndex: { + 'mobile-stepper': '1000', + fab: '1050', + 'speed-dial': '1050', + 'app-bar': '1100', + drawer: '1200', + modal: '1300', + snackbar: '1400', + tooltip: '1500', + }, + + // MUI font family + fontFamily: { + mui: ['Roboto', 'Helvetica', 'Arial', 'sans-serif'], + }, + }, + }, + + plugins: [muiPlugin], +}; diff --git a/packages/mui-tailwind/src/v4.css b/packages/mui-tailwind/src/v4.css new file mode 100644 index 00000000000000..db82b6efee3f77 --- /dev/null +++ b/packages/mui-tailwind/src/v4.css @@ -0,0 +1,232 @@ +/** + * @mui/tailwind — Tailwind CSS v4 integration for Material UI + * + * Usage in your global CSS file: + * + * @layer theme, base, mui.default, components, utilities; + * @import "tailwindcss"; + * @import "@mui/tailwind/v4.css"; + * + * What you get: + * - MUI palette colors as Tailwind color utilities (`text-primary`, `bg-error/50`, …) + * - MUI typography tokens as font utilities (`font-h1`, `font-body2`, …) + * - MUI breakpoints aligned to Tailwind (`sm=600px`, `md=900px`, `lg=1200px`, `xl=1536px`) + * - MUI elevation shadows (`shadow-1` … `shadow-24`) + * - MUI border-radius token (`rounded-mui`) + * - MUI z-index scale (`z-drawer`, `z-modal`, …) + * - `mui-{state}:` and `mui-not-{state}:` variants for MUI component states + */ + +/* ─── Breakpoints ──────────────────────────────────────────────────────────── + Override Tailwind defaults so `sm:`, `md:`, `lg:`, `xl:` map to MUI's values. + `xs` is not defined — it maps to 0px, i.e., the default (no breakpoint). + ─────────────────────────────────────────────────────────────────────────── */ +@theme { + --breakpoint-sm: 37.5rem; /* 600px */ + --breakpoint-md: 56.25rem; /* 900px */ + --breakpoint-lg: 75rem; /* 1200px */ + --breakpoint-xl: 96rem; /* 1536px */ + --breakpoint-2xl: 120rem; /* 1920px */ +} + +/* ─── Design tokens ────────────────────────────────────────────────────────── + `@theme inline` means Tailwind references these variables at the point of + use instead of inlining the values. Variables that reference `--mui-*` + automatically follow the runtime MUI theme (dark mode, custom palette). + ─────────────────────────────────────────────────────────────────────────── */ +@theme inline { + /* Semantic palette -------------------------------------------------------- */ + --color-primary: var(--mui-palette-primary-main); + --color-primary-light: var(--mui-palette-primary-light); + --color-primary-dark: var(--mui-palette-primary-dark); + --color-primary-contrast: var(--mui-palette-primary-contrastText); + + --color-secondary: var(--mui-palette-secondary-main); + --color-secondary-light: var(--mui-palette-secondary-light); + --color-secondary-dark: var(--mui-palette-secondary-dark); + --color-secondary-contrast: var(--mui-palette-secondary-contrastText); + + --color-error: var(--mui-palette-error-main); + --color-error-light: var(--mui-palette-error-light); + --color-error-dark: var(--mui-palette-error-dark); + --color-error-contrast: var(--mui-palette-error-contrastText); + + --color-warning: var(--mui-palette-warning-main); + --color-warning-light: var(--mui-palette-warning-light); + --color-warning-dark: var(--mui-palette-warning-dark); + --color-warning-contrast: var(--mui-palette-warning-contrastText); + + --color-info: var(--mui-palette-info-main); + --color-info-light: var(--mui-palette-info-light); + --color-info-dark: var(--mui-palette-info-dark); + --color-info-contrast: var(--mui-palette-info-contrastText); + + --color-success: var(--mui-palette-success-main); + --color-success-light: var(--mui-palette-success-light); + --color-success-dark: var(--mui-palette-success-dark); + --color-success-contrast: var(--mui-palette-success-contrastText); + + /* Text -------------------------------------------------------------------- */ + --color-text-primary: var(--mui-palette-text-primary); + --color-text-secondary: var(--mui-palette-text-secondary); + --color-text-disabled: var(--mui-palette-text-disabled); + --color-text-icon: var(--mui-palette-text-icon); + + /* Surfaces ---------------------------------------------------------------- */ + --color-background-default: var(--mui-palette-background-default); + --color-background-paper: var(--mui-palette-background-paper); + --color-divider: var(--mui-palette-divider); + + /* Action states ----------------------------------------------------------- */ + --color-action-active: var(--mui-palette-action-active); + --color-action-hover: var(--mui-palette-action-hover); + --color-action-selected: var(--mui-palette-action-selected); + --color-action-disabled: var(--mui-palette-action-disabled); + --color-action-disabled-bg: var(--mui-palette-action-disabledBackground); + --color-action-focus: var(--mui-palette-action-focus); + + /* Grey scale -------------------------------------------------------------- */ + --color-grey-50: var(--mui-palette-grey-50); + --color-grey-100: var(--mui-palette-grey-100); + --color-grey-200: var(--mui-palette-grey-200); + --color-grey-300: var(--mui-palette-grey-300); + --color-grey-400: var(--mui-palette-grey-400); + --color-grey-500: var(--mui-palette-grey-500); + --color-grey-600: var(--mui-palette-grey-600); + --color-grey-700: var(--mui-palette-grey-700); + --color-grey-800: var(--mui-palette-grey-800); + --color-grey-900: var(--mui-palette-grey-900); + --color-grey-A100: var(--mui-palette-grey-A100); + --color-grey-A200: var(--mui-palette-grey-A200); + --color-grey-A400: var(--mui-palette-grey-A400); + --color-grey-A700: var(--mui-palette-grey-A700); + + /* Typography -------------------------------------------------------------- */ + --font-h1: var(--mui-font-h1); + --font-h2: var(--mui-font-h2); + --font-h3: var(--mui-font-h3); + --font-h4: var(--mui-font-h4); + --font-h5: var(--mui-font-h5); + --font-h6: var(--mui-font-h6); + --font-subtitle1: var(--mui-font-subtitle1); + --font-subtitle2: var(--mui-font-subtitle2); + --font-body1: var(--mui-font-body1); + --font-body2: var(--mui-font-body2); + --font-button: var(--mui-font-button); + --font-caption: var(--mui-font-caption); + --font-overline: var(--mui-font-overline); + + /* Letter spacing (typography-specific, not covered by Tailwind defaults) -- */ + --tracking-h1: -0.01562em; + --tracking-h2: -0.00833em; + --tracking-h4: 0.00735em; + --tracking-h6: 0.0075em; + --tracking-subtitle1: 0.00938em; + --tracking-subtitle2: 0.00714em; + --tracking-body1: 0.00938em; + --tracking-body2: 0.01071em; + --tracking-button: 0.02857em; + --tracking-caption: 0.03333em; + --tracking-overline: 0.08333em; + + /* Elevation shadows (0–24 Material Design levels) ------------------------- */ + --shadow-0: var(--mui-shadows-0); + --shadow-1: var(--mui-shadows-1); + --shadow-2: var(--mui-shadows-2); + --shadow-3: var(--mui-shadows-3); + --shadow-4: var(--mui-shadows-4); + --shadow-5: var(--mui-shadows-5); + --shadow-6: var(--mui-shadows-6); + --shadow-7: var(--mui-shadows-7); + --shadow-8: var(--mui-shadows-8); + --shadow-9: var(--mui-shadows-9); + --shadow-10: var(--mui-shadows-10); + --shadow-11: var(--mui-shadows-11); + --shadow-12: var(--mui-shadows-12); + --shadow-13: var(--mui-shadows-13); + --shadow-14: var(--mui-shadows-14); + --shadow-15: var(--mui-shadows-15); + --shadow-16: var(--mui-shadows-16); + --shadow-17: var(--mui-shadows-17); + --shadow-18: var(--mui-shadows-18); + --shadow-19: var(--mui-shadows-19); + --shadow-20: var(--mui-shadows-20); + --shadow-21: var(--mui-shadows-21); + --shadow-22: var(--mui-shadows-22); + --shadow-23: var(--mui-shadows-23); + --shadow-24: var(--mui-shadows-24); + + /* Shape ------------------------------------------------------------------- */ + --radius-mui: calc(var(--mui-shape-borderRadius) * 1px); + + /* Z-index scale ----------------------------------------------------------- */ + --z-mobile-stepper: 1000; + --z-fab: 1050; + --z-speed-dial: 1050; + --z-app-bar: 1100; + --z-drawer: 1200; + --z-modal: 1300; + --z-snackbar: 1400; + --z-tooltip: 1500; + + /* Opacity (action states) ------------------------------------------------- */ + --opacity-hover: var(--mui-palette-action-hoverOpacity); + --opacity-selected: var(--mui-palette-action-selectedOpacity); + --opacity-disabled: var(--mui-palette-action-disabledOpacity); + --opacity-focus: var(--mui-palette-action-focusOpacity); + --opacity-activated: var(--mui-palette-action-activatedOpacity); +} + +/* ─── Custom utilities ─────────────────────────────────────────────────────── + These generate class names from the token namespaces above. + Example: `typography-h1`, `elevation-4` + ─────────────────────────────────────────────────────────────────────────── */ +@utility typography-* { + font: --value(--font-*); +} + +@utility elevation-* { + box-shadow: --value(--shadow-*); +} + +/* ─── State variants ───────────────────────────────────────────────────────── + `mui-{state}:` targets elements that have the corresponding Mui- class. + Use `mui-not-{state}:` to target elements that do NOT have that class. + + Examples: + + + + ─────────────────────────────────────────────────────────────────────────── */ +@custom-variant mui-active (&.Mui-active); +@custom-variant mui-not-active (&:not(.Mui-active)); + +@custom-variant mui-checked (&.Mui-checked); +@custom-variant mui-not-checked (&:not(.Mui-checked)); + +@custom-variant mui-completed (&.Mui-completed); +@custom-variant mui-not-completed (&:not(.Mui-completed)); + +@custom-variant mui-disabled (&.Mui-disabled); +@custom-variant mui-not-disabled (&:not(.Mui-disabled)); + +@custom-variant mui-error (&.Mui-error); +@custom-variant mui-not-error (&:not(.Mui-error)); + +@custom-variant mui-expanded (&.Mui-expanded); +@custom-variant mui-not-expanded (&:not(.Mui-expanded)); + +@custom-variant mui-focused (&.Mui-focused); +@custom-variant mui-not-focused (&:not(.Mui-focused)); + +@custom-variant mui-focus-visible (&.Mui-focusVisible); +@custom-variant mui-not-focus-visible (&:not(.Mui-focusVisible)); + +@custom-variant mui-readonly (&.Mui-readOnly); +@custom-variant mui-not-readonly (&:not(.Mui-readOnly)); + +@custom-variant mui-required (&.Mui-required); +@custom-variant mui-not-required (&:not(.Mui-required)); + +@custom-variant mui-selected (&.Mui-selected); +@custom-variant mui-not-selected (&:not(.Mui-selected)); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8fa95f059d5130..44bf8bc80c0c67 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -305,6 +305,9 @@ importers: '@mui/system': specifier: workspace:^ version: link:../packages/mui-system/build + '@mui/tailwind': + specifier: workspace:^ + version: link:../packages/mui-tailwind/build '@mui/types': specifier: workspace:^ version: link:../packages/mui-types/build @@ -425,9 +428,6 @@ importers: postcss: specifier: ^8.5.14 version: 8.5.15 - postcss-import: - specifier: ^16.1.1 - version: 16.1.1(postcss@8.5.15) prop-types: specifier: ^15.8.1 version: 15.8.1 @@ -1458,6 +1458,13 @@ importers: version: 6.4.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) publishDirectory: build + packages/mui-tailwind: + dependencies: + tailwindcss: + specifier: '>=3.0.0' + version: 4.2.4 + publishDirectory: build + packages/mui-types: dependencies: '@babel/runtime': @@ -1560,7 +1567,7 @@ importers: version: link:../packages/mui-utils/build '@tailwindcss/vite': specifier: ^4.2.4 - version: 4.3.0(vite@7.3.5(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.3.0(vite@8.0.16(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) '@testing-library/dom': specifier: 10.4.1 version: 10.4.1 @@ -9765,12 +9772,6 @@ packages: peerDependencies: postcss: ^8.0.0 - postcss-import@16.1.1: - resolution: {integrity: sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - postcss: ^8.0.0 - postcss-load-config@5.1.0: resolution: {integrity: sha512-G5AJ+IX0aD0dygOE0yFZQ/huFFMSNneyfp0e3/bT05a8OfPC5FUoZRPfGijUdGOJNMewJiwzcHJXFafFzeKFVA==} engines: {node: '>= 18'} @@ -13693,10 +13694,10 @@ snapshots: es-toolkit: 1.46.1 eslint: 10.3.0(jiti@2.6.1) eslint-config-prettier: 10.1.8(eslint@10.3.0(jiti@2.6.1)) - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@10.3.0(jiti@2.6.1)) - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.6.1)) - eslint-plugin-compat: 7.0.2(eslint@10.3.0(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.6.1)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)) + eslint-plugin-compat: 7.0.1(eslint@10.3.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@10.3.0(jiti@2.6.1)) eslint-plugin-mdx: 3.7.0(eslint@10.3.0(jiti@2.6.1)) eslint-plugin-mocha: 11.2.0(eslint@10.3.0(jiti@2.6.1)) @@ -15390,12 +15391,12 @@ snapshots: postcss: 8.5.15 tailwindcss: 4.2.4 - '@tailwindcss/vite@4.3.0(vite@7.3.5(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0))': + '@tailwindcss/vite@4.3.0(vite@8.0.16(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.0 '@tailwindcss/oxide': 4.3.0 tailwindcss: 4.3.0 - vite: 7.3.5(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 8.0.16(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) '@tanstack/query-core@5.100.9': {} @@ -17517,7 +17518,7 @@ snapshots: tinyglobby: 0.2.17 unrs-resolver: 1.9.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -17567,7 +17568,7 @@ snapshots: lodash: 4.18.1 pkg-dir: 5.0.0 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -20870,13 +20871,6 @@ snapshots: - jiti - tsx - postcss-import@16.1.1(postcss@8.5.15): - dependencies: - postcss: 8.5.15 - postcss-value-parser: 4.2.0 - read-cache: 1.0.0 - resolve: 1.22.12 - postcss-load-config@5.1.0(jiti@2.6.1)(postcss@8.5.15)(tsx@4.21.0): dependencies: lilconfig: 3.1.3 diff --git a/tsconfig.json b/tsconfig.json index 11af6fb5ec18bb..dacdd207b475a3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,8 @@ "@mui/internal-markdown/prism": ["./packages-internal/markdown/prism.mjs"], "@mui/styled-engine": ["./packages/mui-styled-engine/src"], "@mui/styled-engine/*": ["./packages/mui-styled-engine/src/*"], + "@mui/tailwind": ["./packages/mui-tailwind/src/preset.js"], + "@mui/tailwind/*": ["./packages/mui-tailwind/src/*"], "@mui/styled-engine-sc": ["./packages/mui-styled-engine-sc/src"], "@mui/styled-engine-sc/*": ["./packages/mui-styled-engine-sc/src/*"], "@mui/system": ["./packages/mui-system/src"], From e23db3cabda56d483dbcae3484370765906696d7 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Wed, 10 Jun 2026 16:11:02 +0300 Subject: [PATCH 02/21] add tailwind usage docs --- .../integrations/tailwindcss/TailwindCard.js | 105 ++++++++++++++ .../integrations/tailwindcss/TailwindCard.tsx | 110 +++++++++++++++ .../tailwindcss/TailwindCard.tsx.preview | 14 ++ .../tailwindcss/TailwindDisabledState.js | 45 ++++++ .../tailwindcss/TailwindDisabledState.tsx | 45 ++++++ .../tailwindcss/TailwindFilterChips.js | 46 ++++++ .../tailwindcss/TailwindFilterChips.tsx | 46 ++++++ .../tailwindcss/tailwindcss-components.md | 133 ++++++++++++++++++ .../tailwindcss/tailwindcss-v4.md | 4 + docs/package.json | 2 +- docs/pages/global.css | 5 + .../tailwindcss/tailwindcss-components.js | 6 + docs/postcss.config.js | 1 - 13 files changed, 560 insertions(+), 2 deletions(-) create mode 100644 docs/data/material/integrations/tailwindcss/TailwindCard.js create mode 100644 docs/data/material/integrations/tailwindcss/TailwindCard.tsx create mode 100644 docs/data/material/integrations/tailwindcss/TailwindCard.tsx.preview create mode 100644 docs/data/material/integrations/tailwindcss/TailwindDisabledState.js create mode 100644 docs/data/material/integrations/tailwindcss/TailwindDisabledState.tsx create mode 100644 docs/data/material/integrations/tailwindcss/TailwindFilterChips.js create mode 100644 docs/data/material/integrations/tailwindcss/TailwindFilterChips.tsx create mode 100644 docs/data/material/integrations/tailwindcss/tailwindcss-components.md create mode 100644 docs/pages/material-ui/integrations/tailwindcss/tailwindcss-components.js diff --git a/docs/data/material/integrations/tailwindcss/TailwindCard.js b/docs/data/material/integrations/tailwindcss/TailwindCard.js new file mode 100644 index 00000000000000..a1201ec17ccf33 --- /dev/null +++ b/docs/data/material/integrations/tailwindcss/TailwindCard.js @@ -0,0 +1,105 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardActions from '@mui/material/CardActions'; +import Button from '@mui/material/Button'; +import Chip from '@mui/material/Chip'; +import Avatar from '@mui/material/Avatar'; +import AvatarGroup from '@mui/material/AvatarGroup'; + +function JobCard({ jobTitle, team, status, applicants, avatarColors }) { + const isActive = status === 'Active'; + return ( + + +
+
+
+ {jobTitle[0]} +
+
+

+ {jobTitle} +

+

+ {team} +

+
+
+ +
+
+
+ + {avatarColors.map((color, i) => ( + + ))} + + + {applicants} applicants + +
+ + Remote + +
+
+ + + + +
+ ); +} + +JobCard.propTypes = { + applicants: PropTypes.number.isRequired, + avatarColors: PropTypes.arrayOf(PropTypes.string).isRequired, + jobTitle: PropTypes.string.isRequired, + status: PropTypes.oneOf(['Active', 'Pending']).isRequired, + team: PropTypes.string.isRequired, +}; + +export default function TailwindCard() { + return ( +
+ + +
+ ); +} diff --git a/docs/data/material/integrations/tailwindcss/TailwindCard.tsx b/docs/data/material/integrations/tailwindcss/TailwindCard.tsx new file mode 100644 index 00000000000000..28db04c4403325 --- /dev/null +++ b/docs/data/material/integrations/tailwindcss/TailwindCard.tsx @@ -0,0 +1,110 @@ +import * as React from 'react'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardActions from '@mui/material/CardActions'; +import Button from '@mui/material/Button'; +import Chip from '@mui/material/Chip'; +import Avatar from '@mui/material/Avatar'; +import AvatarGroup from '@mui/material/AvatarGroup'; + +interface JobCardProps { + jobTitle: string; + team: string; + status: 'Active' | 'Pending'; + applicants: number; + avatarColors: string[]; +} + +function JobCard({ + jobTitle, + team, + status, + applicants, + avatarColors, +}: JobCardProps) { + const isActive = status === 'Active'; + return ( + + +
+
+
+ {jobTitle[0]} +
+
+

+ {jobTitle} +

+

+ {team} +

+
+
+ +
+
+
+ + {avatarColors.map((color, i) => ( + + ))} + + + {applicants} applicants + +
+ + Remote + +
+
+ + + + +
+ ); +} + +export default function TailwindCard() { + return ( +
+ + +
+ ); +} diff --git a/docs/data/material/integrations/tailwindcss/TailwindCard.tsx.preview b/docs/data/material/integrations/tailwindcss/TailwindCard.tsx.preview new file mode 100644 index 00000000000000..cf198deb4e2606 --- /dev/null +++ b/docs/data/material/integrations/tailwindcss/TailwindCard.tsx.preview @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/docs/data/material/integrations/tailwindcss/TailwindDisabledState.js b/docs/data/material/integrations/tailwindcss/TailwindDisabledState.js new file mode 100644 index 00000000000000..7f47b691debe68 --- /dev/null +++ b/docs/data/material/integrations/tailwindcss/TailwindDisabledState.js @@ -0,0 +1,45 @@ +import * as React from 'react'; +import Slider from '@mui/material/Slider'; +import Switch from '@mui/material/Switch'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Stack from '@mui/material/Stack'; + +export default function TailwindDisabledState() { + return ( + +
+

+ Volume — enabled vs disabled +

+ + {/* Tailwind's `mui-disabled:` variant adds opacity only when the + component has the `Mui-disabled` class applied by MUI */} + + + +
+
+

+ Notifications — enabled vs disabled +

+ + } + /> + } + /> + +
+
+ ); +} diff --git a/docs/data/material/integrations/tailwindcss/TailwindDisabledState.tsx b/docs/data/material/integrations/tailwindcss/TailwindDisabledState.tsx new file mode 100644 index 00000000000000..b2d80625f31186 --- /dev/null +++ b/docs/data/material/integrations/tailwindcss/TailwindDisabledState.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import Slider from '@mui/material/Slider'; +import Switch from '@mui/material/Switch'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Stack from '@mui/material/Stack'; + +export default function TailwindDisabledState() { + return ( + +
+

+ Volume — enabled vs disabled +

+ + {/* Tailwind's `mui-disabled:` variant adds opacity only when the + component has the `Mui-disabled` class applied by MUI */} + + + +
+
+

+ Notifications — enabled vs disabled +

+ + } + /> + } + /> + +
+
+ ); +} diff --git a/docs/data/material/integrations/tailwindcss/TailwindFilterChips.js b/docs/data/material/integrations/tailwindcss/TailwindFilterChips.js new file mode 100644 index 00000000000000..81e08d8cd930b2 --- /dev/null +++ b/docs/data/material/integrations/tailwindcss/TailwindFilterChips.js @@ -0,0 +1,46 @@ +import * as React from 'react'; +import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; + +const tags = ['React', 'TypeScript', 'CSS', 'Performance', 'Testing']; + +export default function TailwindFilterChips() { + const [selected, setSelected] = React.useState(['React', 'TypeScript']); + + const handleChange = (_, newSelected) => { + setSelected(newSelected); + }; + + return ( +
+ + {tags.map((tag) => ( + + {tag} + + ))} + +

+ {selected.length === 0 + ? 'No filters active' + : `Active: ${selected.join(', ')}`} +

+
+ ); +} diff --git a/docs/data/material/integrations/tailwindcss/TailwindFilterChips.tsx b/docs/data/material/integrations/tailwindcss/TailwindFilterChips.tsx new file mode 100644 index 00000000000000..9ad5eb006faf0c --- /dev/null +++ b/docs/data/material/integrations/tailwindcss/TailwindFilterChips.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; + +const tags = ['React', 'TypeScript', 'CSS', 'Performance', 'Testing']; + +export default function TailwindFilterChips() { + const [selected, setSelected] = React.useState(['React', 'TypeScript']); + + const handleChange = (_: React.MouseEvent, newSelected: string[]) => { + setSelected(newSelected); + }; + + return ( +
+ + {tags.map((tag) => ( + + {tag} + + ))} + +

+ {selected.length === 0 + ? 'No filters active' + : `Active: ${selected.join(', ')}`} +

+
+ ); +} diff --git a/docs/data/material/integrations/tailwindcss/tailwindcss-components.md b/docs/data/material/integrations/tailwindcss/tailwindcss-components.md new file mode 100644 index 00000000000000..4e7602f7a71a62 --- /dev/null +++ b/docs/data/material/integrations/tailwindcss/tailwindcss-components.md @@ -0,0 +1,133 @@ +# Using the @mui/tailwind package + +

The @mui/tailwind package provides a Tailwind CSS preset, plugin, and v4 CSS file that wire MUI's design tokens and component state classes into Tailwind.

+ +## Installation + +```bash +npm install @mui/tailwind +``` + +## Setup + +### Tailwind v4 + +Import the CSS file in your global stylesheet after your `@import "tailwindcss"` line: + +```css title="global.css" +@layer theme, base, mui.default, components, utilities; +@import 'tailwindcss'; +@import '@mui/tailwind/v4.css'; +``` + +`v4.css` adds three things: + +1. **Token mapping** — maps `--mui-palette-*`, `--mui-shadows-*`, and other MUI CSS variables to Tailwind theme tokens so utilities like `text-primary`, `bg-error/20`, and `shadow-8` work out of the box (requires `CssVarsProvider`) +2. **State variants** — adds `mui-{state}:` and `mui-not-{state}:` modifier classes for every MUI component state class +3. **MUI-aligned breakpoints** — sets `sm=600px`, `md=900px`, `lg=1200px`, `xl=1536px` + +### Tailwind v3 + +Add the preset to `tailwind.config.js`: + +```js title="tailwind.config.js" +module.exports = { + presets: [require('@mui/tailwind')], +}; +``` + +The preset extends the Tailwind theme with MUI color tokens (using channel variables for opacity modifier support) and includes the plugin automatically. + +If you only need the state variant plugin without the full preset: + +```js title="tailwind.config.js" +module.exports = { + plugins: [require('@mui/tailwind/plugin')], +}; +``` + +## State variants + +The `@mui/tailwind` plugin adds `mui-{state}:` modifier classes that target MUI component state classes (`Mui-disabled`, `Mui-selected`, `Mui-error`, etc.). + +| Variant | Targets | +| :------------------- | :------------------ | +| `mui-disabled:` | `.Mui-disabled` | +| `mui-selected:` | `.Mui-selected` | +| `mui-error:` | `.Mui-error` | +| `mui-focused:` | `.Mui-focused` | +| `mui-focus-visible:` | `.Mui-focusVisible` | +| `mui-checked:` | `.Mui-checked` | +| `mui-expanded:` | `.Mui-expanded` | +| `mui-active:` | `.Mui-active` | +| `mui-readonly:` | `.Mui-readOnly` | +| `mui-required:` | `.Mui-required` | +| `mui-completed:` | `.Mui-completed` | + +Every variant has a `mui-not-{state}:` counterpart that matches when the class is absent. + +## Viewing the demos + +Start the docs dev server: + +```bash +pnpm docs:dev +``` + +Then open: http://localhost:3000/material-ui/integrations/tailwindcss/tailwindcss-components/ + + +### Filter chips with `mui-selected` + +The following demo shows `mui-selected:` styling applied to a `ToggleButtonGroup`. +MUI adds the `Mui-selected` class to each `ToggleButton` when it is active, +and the Tailwind variant applies the indigo styles only to those elements. + +{{"demo": "TailwindFilterChips.js"}} + +### Disabled state with `mui-disabled` + +`mui-disabled:` targets elements where MUI has applied the `Mui-disabled` class. +Because the variant selector is specific to the disabled state, the modifier +applies only to the disabled component, leaving the enabled one untouched. + +{{"demo": "TailwindDisabledState.js"}} + +## Styling components with `className` and `slotProps` + +Use `className` to apply Tailwind utilities to a component's root element. +Use `slotProps.{slot}.className` to reach interior elements. + +The following demo builds a job-listing card using only Tailwind utilities +for visual design while keeping MUI components for structure and accessibility. + +{{"demo": "TailwindCard.js"}} + +## Layer ordering + +For Tailwind utilities to override MUI component styles **without `!important`**, MUI's styles must be inside a CSS layer with lower priority than Tailwind's `utilities` layer. + +With the Emotion-based setup, enable `enableCssLayer` to put MUI styles in `@layer mui`: + +```css title="global.css" +@layer theme, base, mui, components, utilities; +@import 'tailwindcss'; +``` + +```tsx title="app/layout.tsx (Next.js App Router)" +import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter'; + +export default function RootLayout({ children }) { + return ( + + + + {children} + + + + ); +} +``` + +With the CSS-based setup (`@mui/styled-engine-noop`), MUI component styles are already in `@layer mui.default`, so the layer ordering is automatic — Tailwind utilities win without any `!important`. diff --git a/docs/data/material/integrations/tailwindcss/tailwindcss-v4.md b/docs/data/material/integrations/tailwindcss/tailwindcss-v4.md index 5e71ba7454ed5e..53168d1ed0e5e5 100644 --- a/docs/data/material/integrations/tailwindcss/tailwindcss-v4.md +++ b/docs/data/material/integrations/tailwindcss/tailwindcss-v4.md @@ -151,6 +151,10 @@ Now you should see the autocomplete and syntax highlighting features when using ## Extend Material UI classes +:::info +Instead of copying this snippet, install the [`@mui/tailwind`](/material-ui/integrations/tailwindcss/tailwindcss-components/) package which ships the same token mapping alongside MUI state variants (`mui-disabled:`, `mui-selected:`, and more) and MUI-aligned breakpoints in a single import. +::: + If you want to use Material UI theme tokens in your Tailwind CSS classes, copy the snippet below into your CSS file. ```css title="global.css" diff --git a/docs/package.json b/docs/package.json index f1df7b7817fdd9..106827e4ba04bc 100644 --- a/docs/package.json +++ b/docs/package.json @@ -35,6 +35,7 @@ "@mui/styled-engine-sc": "workspace:^", "@mui/stylis-plugin-rtl": "workspace:^", "@mui/system": "workspace:^", + "@mui/tailwind": "workspace:^", "@mui/types": "workspace:^", "@mui/utils": "workspace:^", "@mui/x-charts": "9.5.0", @@ -75,7 +76,6 @@ "notistack": "3.0.2", "nprogress": "^0.2.0", "postcss": "^8.5.14", - "postcss-import": "^16.1.1", "prop-types": "^15.8.1", "react": "^19.2.6", "react-dom": "^19.2.6", diff --git a/docs/pages/global.css b/docs/pages/global.css index 2e7ff043d25b9f..4cbd2e7c8409d5 100644 --- a/docs/pages/global.css +++ b/docs/pages/global.css @@ -1,3 +1,8 @@ +@layer theme, base, mui.default, components, utilities; + @import 'tailwindcss/theme.css' layer(theme); @import 'tailwindcss/utilities.css' layer(utilities); +@import '@mui/tailwind/v4.css'; + @config '../tailwind.config.mjs'; + diff --git a/docs/pages/material-ui/integrations/tailwindcss/tailwindcss-components.js b/docs/pages/material-ui/integrations/tailwindcss/tailwindcss-components.js new file mode 100644 index 00000000000000..596e915c6d2a35 --- /dev/null +++ b/docs/pages/material-ui/integrations/tailwindcss/tailwindcss-components.js @@ -0,0 +1,6 @@ +import { MarkdownDocs } from '@mui/internal-core-docs/MarkdownDocs'; +import * as pageProps from 'docs/data/material/integrations/tailwindcss/tailwindcss-components.md?muiMarkdown'; + +export default function Page() { + return ; +} diff --git a/docs/postcss.config.js b/docs/postcss.config.js index ac6e4da4cd6a1c..e5640725a9e329 100644 --- a/docs/postcss.config.js +++ b/docs/postcss.config.js @@ -1,6 +1,5 @@ module.exports = { plugins: { - 'postcss-import': {}, '@tailwindcss/postcss': {}, }, }; From 60b55ae49ac3ef50bd03012ea1d74a8ab9d03c00 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Wed, 10 Jun 2026 16:15:54 +0300 Subject: [PATCH 03/21] prettier --- .../material/integrations/tailwindcss/tailwindcss-components.md | 1 - docs/pages/global.css | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/data/material/integrations/tailwindcss/tailwindcss-components.md b/docs/data/material/integrations/tailwindcss/tailwindcss-components.md index 4e7602f7a71a62..5db060463c6b9a 100644 --- a/docs/data/material/integrations/tailwindcss/tailwindcss-components.md +++ b/docs/data/material/integrations/tailwindcss/tailwindcss-components.md @@ -76,7 +76,6 @@ pnpm docs:dev Then open: http://localhost:3000/material-ui/integrations/tailwindcss/tailwindcss-components/ - ### Filter chips with `mui-selected` The following demo shows `mui-selected:` styling applied to a `ToggleButtonGroup`. diff --git a/docs/pages/global.css b/docs/pages/global.css index 4cbd2e7c8409d5..41dcae1c8fd3fd 100644 --- a/docs/pages/global.css +++ b/docs/pages/global.css @@ -5,4 +5,3 @@ @import '@mui/tailwind/v4.css'; @config '../tailwind.config.mjs'; - From a43130bc567a754d12872cf192b17294d3b72f10 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Wed, 10 Jun 2026 16:16:34 +0300 Subject: [PATCH 04/21] pnpm install & dedupe --- pnpm-lock.yaml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44bf8bc80c0c67..0cfdaf6269c537 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -307,7 +307,7 @@ importers: version: link:../packages/mui-system/build '@mui/tailwind': specifier: workspace:^ - version: link:../packages/mui-tailwind/build + version: link:../packages/mui-tailwind '@mui/types': specifier: workspace:^ version: link:../packages/mui-types/build @@ -1463,7 +1463,6 @@ importers: tailwindcss: specifier: '>=3.0.0' version: 4.2.4 - publishDirectory: build packages/mui-types: dependencies: @@ -1567,7 +1566,7 @@ importers: version: link:../packages/mui-utils/build '@tailwindcss/vite': specifier: ^4.2.4 - version: 4.3.0(vite@8.0.16(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.3.0(vite@7.3.5(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) '@testing-library/dom': specifier: 10.4.1 version: 10.4.1 @@ -13697,7 +13696,7 @@ snapshots: eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)) eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)) eslint-plugin-compat: 7.0.1(eslint@10.3.0(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@10.3.0(jiti@2.6.1)) eslint-plugin-mdx: 3.7.0(eslint@10.3.0(jiti@2.6.1)) eslint-plugin-mocha: 11.2.0(eslint@10.3.0(jiti@2.6.1)) @@ -15391,12 +15390,12 @@ snapshots: postcss: 8.5.15 tailwindcss: 4.2.4 - '@tailwindcss/vite@4.3.0(vite@8.0.16(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0))': + '@tailwindcss/vite@4.3.0(vite@7.3.5(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.0 '@tailwindcss/oxide': 4.3.0 tailwindcss: 4.3.0 - vite: 8.0.16(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 7.3.5(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) '@tanstack/query-core@5.100.9': {} @@ -17518,7 +17517,7 @@ snapshots: tinyglobby: 0.2.17 unrs-resolver: 1.9.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -17568,7 +17567,7 @@ snapshots: lodash: 4.18.1 pkg-dir: 5.0.0 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 From 2c503015061ccc9192b1ee3950f9c07ed88bd0fc Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Fri, 12 Jun 2026 11:34:27 +0300 Subject: [PATCH 05/21] change Slider to import css modules + generate transition variables --- packages/mui-material/src/Slider/Slider.js | 482 ++---------------- .../mui-material/src/Slider/Slider.module.css | 462 +++++++++++++++++ .../src/styles/shouldSkipGeneratingVar.ts | 2 +- .../mui-system/src/cssVars/cssVarsParser.ts | 4 + .../src/composeClasses/composeClasses.ts | 9 +- 5 files changed, 527 insertions(+), 432 deletions(-) create mode 100644 packages/mui-material/src/Slider/Slider.module.css diff --git a/packages/mui-material/src/Slider/Slider.js b/packages/mui-material/src/Slider/Slider.js index bdc7e2c3d992e5..2f1a80612f0b97 100644 --- a/packages/mui-material/src/Slider/Slider.js +++ b/packages/mui-material/src/Slider/Slider.js @@ -8,15 +8,13 @@ import { useRtl } from '@mui/system/RtlProvider'; import isHostComponent from '@mui/utils/isHostComponent'; import { useSlider, valueToPercent } from './useSlider'; import { styled } from '../zero-styled'; -import memoTheme from '../utils/memoTheme'; import { useDefaultProps } from '../DefaultPropsProvider'; import useSlot from '../utils/useSlot'; import slotShouldForwardProp from '../styles/slotShouldForwardProp'; import capitalize from '../utils/capitalize'; -import createSimplePaletteValueFilter from '../utils/createSimplePaletteValueFilter'; import BaseSliderValueLabel from './SliderValueLabel'; -import sliderClasses, { getSliderUtilityClass } from './sliderClasses'; -import { getTransitionStyles } from '../transitions/utils'; +import { getSliderUtilityClass } from './sliderClasses'; +import cssModules from './Slider.module.css'; function Identity(x) { return x; @@ -38,374 +36,35 @@ export const SliderRoot = styled('span', { ownerState.track === false && styles.trackFalse, ]; }, -})( - memoTheme(({ theme }) => ({ - borderRadius: 12, - boxSizing: 'content-box', - display: 'inline-block', - position: 'relative', - cursor: 'pointer', - touchAction: 'none', - WebkitTapHighlightColor: 'transparent', - '@media print': { - colorAdjust: 'exact', - }, - [`&.${sliderClasses.disabled}`]: { - pointerEvents: 'none', - cursor: 'default', - color: (theme.vars || theme).palette.grey[400], - }, - [`&.${sliderClasses.dragging}`]: { - [`& .${sliderClasses.thumb}, & .${sliderClasses.track}`]: { - transition: 'none', - }, - }, - variants: [ - ...Object.entries(theme.palette) - .filter(createSimplePaletteValueFilter()) - .map(([color]) => ({ - props: { color }, - style: { - color: (theme.vars || theme).palette[color].main, - }, - })), - { - props: { orientation: 'horizontal' }, - style: { - height: 4, - width: '100%', - padding: '13px 0', - // The primary input mechanism of the device includes a pointing device of limited accuracy. - '@media (pointer: coarse)': { - // Reach 42px touch target, about ~8mm on screen. - padding: '20px 0', - }, - }, - }, - { - props: { orientation: 'horizontal', size: 'small' }, - style: { - height: 2, - }, - }, - { - props: { orientation: 'horizontal', marked: true }, - style: { - marginBottom: 20, - }, - }, - { - props: { orientation: 'vertical' }, - style: { - height: '100%', - width: 4, - padding: '0 13px', - // The primary input mechanism of the device includes a pointing device of limited accuracy. - '@media (pointer: coarse)': { - // Reach 42px touch target, about ~8mm on screen. - padding: '0 20px', - }, - }, - }, - { - props: { orientation: 'vertical', size: 'small' }, - style: { - width: 2, - }, - }, - { - props: { orientation: 'vertical', marked: true }, - style: { - marginRight: 44, - }, - }, - ], - })), -); +})({}); export const SliderRail = styled('span', { name: 'MuiSlider', slot: 'Rail', -})({ - display: 'block', - position: 'absolute', - borderRadius: 'inherit', - backgroundColor: 'currentColor', - opacity: 0.38, - '@media (forced-colors: active)': { - border: '1px solid transparent', - boxSizing: 'border-box', - }, - variants: [ - { - props: { orientation: 'horizontal' }, - style: { - width: '100%', - height: 'inherit', - top: '50%', - transform: 'translateY(-50%)', - }, - }, - { - props: { orientation: 'vertical' }, - style: { - height: '100%', - width: 'inherit', - left: '50%', - transform: 'translateX(-50%)', - }, - }, - { - props: { track: 'inverted' }, - style: { - opacity: 1, - }, - }, - ], -}); +})({}); export const SliderTrack = styled('span', { name: 'MuiSlider', slot: 'Track', -})( - memoTheme(({ theme }) => { - return { - display: 'block', - position: 'absolute', - borderRadius: 'inherit', - border: '1px solid currentColor', - backgroundColor: 'currentColor', - ...getTransitionStyles(theme, ['left', 'width', 'bottom', 'height'], { - duration: theme.transitions.duration.shortest, - }), - variants: [ - { - props: { size: 'small' }, - style: { - '@media (forced-colors: none)': { - border: 'none', - }, - }, - }, - { - props: { orientation: 'horizontal' }, - style: { - height: 'inherit', - top: '50%', - transform: 'translateY(-50%)', - }, - }, - { - props: { orientation: 'vertical' }, - style: { - width: 'inherit', - left: '50%', - transform: 'translateX(-50%)', - }, - }, - { - props: { track: false }, - style: { - display: 'none', - }, - }, - ...Object.entries(theme.palette) - .filter(createSimplePaletteValueFilter()) - .map(([color]) => ({ - props: { color, track: 'inverted' }, - style: { - ...(theme.vars - ? { - backgroundColor: theme.vars.palette.Slider[`${color}Track`], - borderColor: theme.vars.palette.Slider[`${color}Track`], - } - : { - backgroundColor: theme.lighten(theme.palette[color].main, 0.62), - borderColor: theme.lighten(theme.palette[color].main, 0.62), - ...theme.applyStyles('dark', { - backgroundColor: theme.darken(theme.palette[color].main, 0.5), - }), - ...theme.applyStyles('dark', { - borderColor: theme.darken(theme.palette[color].main, 0.5), - }), - }), - }, - })), - ], - }; - }), -); +})({}); export const SliderThumb = styled('span', { name: 'MuiSlider', slot: 'Thumb', -})( - memoTheme(({ theme }) => ({ - position: 'absolute', - width: 20, - height: 20, - boxSizing: 'border-box', - borderRadius: '50%', - outline: 0, - backgroundColor: 'currentColor', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - ...getTransitionStyles(theme, ['box-shadow', 'left', 'bottom'], { - duration: theme.transitions.duration.shortest, - }), - '@media (forced-colors: active)': { - border: '1px solid ButtonBorder', - }, - '&::before': { - position: 'absolute', - content: '""', - borderRadius: 'inherit', - width: '100%', - height: '100%', - boxShadow: (theme.vars || theme).shadows[2], - }, - '&::after': { - position: 'absolute', - content: '""', - borderRadius: '50%', - // 42px is the hit target - width: 42, - height: 42, - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - }, - [`&.${sliderClasses.disabled}`]: { - '&:hover': { - boxShadow: 'none', - }, - }, - variants: [ - { - props: { size: 'small' }, - style: { - width: 12, - height: 12, - '&::before': { - boxShadow: 'none', - }, - }, - }, - { - props: { orientation: 'horizontal' }, - style: { - top: '50%', - transform: 'translate(-50%, -50%)', - }, - }, - { - props: { orientation: 'vertical' }, - style: { - left: '50%', - transform: 'translate(-50%, 50%)', - }, - }, - ...Object.entries(theme.palette) - .filter(createSimplePaletteValueFilter()) - .map(([color]) => ({ - props: { color }, - style: { - [`&:hover, &.${sliderClasses.focusVisible}`]: { - boxShadow: `0px 0px 0px 8px ${theme.alpha((theme.vars || theme).palette[color].main, 0.16)}`, - '@media (hover: none)': { - boxShadow: 'none', - }, - }, - [`&.${sliderClasses.active}`]: { - boxShadow: `0px 0px 0px 14px ${theme.alpha((theme.vars || theme).palette[color].main, 0.16)}`, - }, - }, - })), - ], - })), -); + overridesResolver: (props, styles) => { + const { ownerState } = props; + return [ + styles.thumb, + styles[`thumbColor${capitalize(ownerState.color)}`], + ownerState.size !== 'medium' && styles[`thumbSize${capitalize(ownerState.size)}`], + ]; + }, +})({}); const SliderValueLabel = styled(BaseSliderValueLabel, { name: 'MuiSlider', slot: 'ValueLabel', -})( - memoTheme(({ theme }) => ({ - zIndex: 1, - whiteSpace: 'nowrap', - ...theme.typography.body2, - fontWeight: 500, - ...getTransitionStyles(theme, ['transform'], { - duration: theme.transitions.duration.shortest, - }), - position: 'absolute', - backgroundColor: (theme.vars || theme).palette.grey[600], - borderRadius: 2, - color: (theme.vars || theme).palette.common.white, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - padding: '0.25rem 0.75rem', - variants: [ - { - props: { orientation: 'horizontal' }, - style: { - transform: 'translateY(-100%) scale(0)', - top: '-10px', - transformOrigin: 'bottom center', - '&::before': { - position: 'absolute', - content: '""', - width: 8, - height: 8, - transform: 'translate(-50%, 50%) rotate(45deg)', - backgroundColor: 'inherit', - bottom: 0, - left: '50%', - }, - [`&.${sliderClasses.valueLabelOpen}`]: { - transform: 'translateY(-100%) scale(1)', - }, - }, - }, - { - props: { orientation: 'vertical' }, - style: { - transform: 'translateY(-50%) scale(0)', - right: '30px', - top: '50%', - transformOrigin: 'right center', - '&::before': { - position: 'absolute', - content: '""', - width: 8, - height: 8, - transform: 'translate(-50%, -50%) rotate(45deg)', - backgroundColor: 'inherit', - right: -8, - top: '50%', - }, - [`&.${sliderClasses.valueLabelOpen}`]: { - transform: 'translateY(-50%) scale(1)', - }, - }, - }, - { - props: { size: 'small' }, - style: { - fontSize: theme.typography.pxToRem(12), - padding: '0.25rem 0.5rem', - }, - }, - { - props: { orientation: 'vertical', size: 'small' }, - style: { - right: '20px', - }, - }, - ], - })), -); +})({}); SliderValueLabel.propTypes /* remove-proptypes */ = { // ┌────────────────────────────── Warning ──────────────────────────────┐ @@ -441,79 +100,13 @@ export const SliderMark = styled('span', { return [styles.mark, markActive && styles.markActive]; }, -})( - memoTheme(({ theme }) => ({ - position: 'absolute', - width: 2, - height: 2, - borderRadius: 1, - backgroundColor: 'currentColor', - variants: [ - { - props: { orientation: 'horizontal' }, - style: { - top: '50%', - transform: 'translate(-1px, -50%)', - }, - }, - { - props: { orientation: 'vertical' }, - style: { - left: '50%', - transform: 'translate(-50%, 1px)', - }, - }, - { - props: { markActive: true }, - style: { - backgroundColor: (theme.vars || theme).palette.background.paper, - opacity: 0.8, - }, - }, - ], - })), -); +})({}); export const SliderMarkLabel = styled('span', { name: 'MuiSlider', slot: 'MarkLabel', shouldForwardProp: (prop) => slotShouldForwardProp(prop) && prop !== 'markLabelActive', -})( - memoTheme(({ theme }) => ({ - ...theme.typography.body2, - color: (theme.vars || theme).palette.text.secondary, - position: 'absolute', - whiteSpace: 'nowrap', - variants: [ - { - props: { orientation: 'horizontal' }, - style: { - top: 30, - transform: 'translateX(-50%)', - '@media (pointer: coarse)': { - top: 40, - }, - }, - }, - { - props: { orientation: 'vertical' }, - style: { - left: 36, - transform: 'translateY(50%)', - '@media (pointer: coarse)': { - left: 44, - }, - }, - }, - { - props: { markLabelActive: true }, - style: { - color: (theme.vars || theme).palette.text.primary, - }, - }, - ], - })), -); +})({}); const useUtilityClasses = (ownerState) => { const { disabled, dragging, marked, orientation, track, classes, color, size } = ownerState; @@ -524,26 +117,55 @@ const useUtilityClasses = (ownerState) => { disabled && 'disabled', dragging && 'dragging', marked && 'marked', + orientation === 'horizontal' && 'horizontal', orientation === 'vertical' && 'vertical', track === 'inverted' && 'trackInverted', track === false && 'trackFalse', color && `color${capitalize(color)}`, size && `size${capitalize(size)}`, ], - rail: ['rail'], - track: ['track'], + rail: [ + 'rail', + orientation === 'horizontal' && 'horizontal', + track === 'inverted' && 'trackInverted', + orientation === 'vertical' && 'vertical', + ], + track: [ + 'track', + orientation === 'horizontal' && 'horizontal', + track === 'inverted' && 'trackInverted', + orientation === 'vertical' && 'vertical', + color && `color${capitalize(color)}`, + size && `size${capitalize(size)}`, + ], mark: ['mark'], markActive: ['markActive'], - markLabel: ['markLabel'], + markLabel: [ + 'markLabel', + orientation === 'horizontal' && 'horizontal', + orientation === 'vertical' && 'vertical', + ], markLabelActive: ['markLabelActive'], - valueLabel: ['valueLabel'], - thumb: ['thumb', disabled && 'disabled'], + valueLabel: [ + 'valueLabel', + size && `size${capitalize(size)}`, + orientation === 'horizontal' && 'horizontal', + orientation === 'vertical' && 'vertical', + ], + thumb: [ + 'thumb', + disabled && 'disabled', + size && `thumbSize${capitalize(size)}`, + color && `thumbColor${capitalize(color)}`, + orientation === 'horizontal' && 'horizontal', + orientation === 'vertical' && 'vertical', + ], active: ['active'], disabled: ['disabled'], focusVisible: ['focusVisible'], }; - return composeClasses(slots, getSliderUtilityClass, classes); + return composeClasses(slots, getSliderUtilityClass, classes, cssModules); }; const Forward = ({ children }) => children; diff --git a/packages/mui-material/src/Slider/Slider.module.css b/packages/mui-material/src/Slider/Slider.module.css new file mode 100644 index 00000000000000..8eaadb4c859f5e --- /dev/null +++ b/packages/mui-material/src/Slider/Slider.module.css @@ -0,0 +1,462 @@ +@layer mui.default { + .MuiSlider-root { + border-radius: 12px; + box-sizing: content-box; + display: inline-block; + position: relative; + cursor: pointer; + touch-action: none; + -webkit-tap-highlight-color: transparent; + } + + @media print { + .MuiSlider-root { + print-color-adjust: exact; + -webkit-print-color-adjust: exact; + } + } + + .MuiSlider-root.MuiSlider-trackFalse .MuiSlider-track { + display: none; + } + + .MuiSlider-root.MuiSlider-dragging .MuiSlider-thumb, + .MuiSlider-root.MuiSlider-dragging .MuiSlider-track { + transition: none; + } + + .MuiSlider-root.MuiSlider-colorPrimary { + color: var(--mui-palette-primary-main); + } + .MuiSlider-root.MuiSlider-colorSecondary { + color: var(--mui-palette-secondary-main); + } + .MuiSlider-root.MuiSlider-colorWarning { + color: var(--mui-palette-warning-main); + } + .MuiSlider-root.MuiSlider-colorError { + color: var(--mui-palette-error-main); + } + .MuiSlider-root.MuiSlider-colorSuccess { + color: var(--mui-palette-success-main); + } + .MuiSlider-root.MuiSlider-colorInfo { + color: var(--mui-palette-info-main); + } + + .MuiSlider-root.MuiSlider-horizontal { + height: 4px; + width: 100%; + padding-block: 13px; + } + + @media (pointer: coarse) { + .MuiSlider-root.MuiSlider-horizontal { + padding-block: 20px; + } + } + + .MuiSlider-root.MuiSlider-horizontal.MuiSlider-sizeSmall { + height: 2px; + } + + .MuiSlider-root.MuiSlider-horizontal.MuiSlider-marked { + margin-block-end: 20px; + } + + .MuiSlider-root.MuiSlider-vertical { + height: 100%; + width: 4px; + padding-inline: 13px; + } + + @media (pointer: coarse) { + .MuiSlider-root.MuiSlider-vertical { + padding-inline: 20px; + } + } + + .MuiSlider-root.MuiSlider-vertical.MuiSlider-sizeSmall { + width: 2px; + } + + .MuiSlider-root.MuiSlider-vertical.MuiSlider-marked { + margin-inline-end: 44px; + } + + .MuiSlider-root.Mui-disabled { + pointer-events: none; + cursor: default; + color: var(--mui-palette-grey-400); + } + + .MuiSlider-rail { + display: block; + position: absolute; + border-radius: inherit; + background-color: currentColor; + opacity: 0.38; + } + + @media (forced-colors: active) { + .MuiSlider-rail { + border: 1px solid transparent; + box-sizing: border-box; + } + } + + .MuiSlider-rail.MuiSlider-horizontal { + width: 100%; + height: inherit; + inset-block-start: 50%; + transform: translateY(-50%); + } + + .MuiSlider-rail.MuiSlider-vertical { + height: 100%; + width: inherit; + inset-inline-start: 50%; + transform: translateX(-50%); + } + + .MuiSlider-rail.MuiSlider-trackInverted { + opacity: 1; + } + + .MuiSlider-track { + display: block; + position: absolute; + border-radius: inherit; + border: 1px solid currentColor; + background-color: currentColor; + transition: + left var(--mui-transitions-duration-shortest) var(--mui-transitions-easing-easeInOut), + width var(--mui-transitions-duration-shortest) var(--mui-transitions-easing-easeInOut), + bottom var(--mui-transitions-duration-shortest) var(--mui-transitions-easing-easeInOut), + height var(--mui-transitions-duration-shortest) var(--mui-transitions-easing-easeInOut); + } + + .MuiSlider-track.MuiSlider-trackInverted.MuiSlider-colorPrimary { + background-color: var(--mui-palette-Slider-primaryTrack); + border-color: var(--mui-palette-Slider-primaryTrack); + } + + .MuiSlider-track.MuiSlider-trackInverted.MuiSlider-colorSecondary { + background-color: var(--mui-palette-Slider-secondaryTrack); + border-color: var(--mui-palette-Slider-secondaryTrack); + } + + .MuiSlider-track.MuiSlider-trackInverted.MuiSlider-colorError { + background-color: var(--mui-palette-Slider-errorTrack); + border-color: var(--mui-palette-Slider-errorTrack); + } + + .MuiSlider-track.MuiSlider-trackInverted.MuiSlider-colorInfo { + background-color: var(--mui-palette-Slider-infoTrack); + border-color: var(--mui-palette-Slider-infoTrack); + } + + .MuiSlider-track.MuiSlider-trackInverted.MuiSlider-colorSuccess { + background-color: var(--mui-palette-Slider-successTrack); + border-color: var(--mui-palette-Slider-successTrack); + } + + .MuiSlider-track.MuiSlider-trackInverted.MuiSlider-colorWarning { + background-color: var(--mui-palette-Slider-warningTrack); + border-color: var(--mui-palette-Slider-warningTrack); + } + + @media (forced-colors: none) { + .MuiSlider-track.MuiSlider-sizeSmall { + border: none; + } + } + + .MuiSlider-track.MuiSlider-horizontal { + height: inherit; + inset-block-start: 50%; + transform: translateY(-50%); + } + + .MuiSlider-track.MuiSlider-vertical { + width: inherit; + inset-inline-start: 50%; + transform: translateX(-50%); + } + + .MuiSlider-track.MuiSlider-trackFalse { + display: none; + } + + .MuiSlider-thumb { + position: absolute; + width: 20px; + height: 20px; + box-sizing: border-box; + border-radius: 50%; + outline: 0; + background-color: currentColor; + display: flex; + align-items: center; + justify-content: center; + transition: + box-shadow var(--mui-transitions-duration-shortest) var(--mui-transitions-easing-easeInOut), + left var(--mui-transitions-duration-shortest) var(--mui-transitions-easing-easeInOut), + bottom var(--mui-transitions-duration-shortest) var(--mui-transitions-easing-easeInOut); + } + + @media (forced-colors: active) { + .MuiSlider-thumb { + border: 1px solid ButtonBorder; + } + } + + .MuiSlider-thumb::before { + position: absolute; + content: ''; + border-radius: inherit; + width: 100%; + height: 100%; + box-shadow: var(--mui-shadows-2); + } + + .MuiSlider-thumb::after { + position: absolute; + content: ''; + border-radius: 50%; + width: 42px; + height: 42px; + inset-block-start: 50%; + inset-inline-start: 50%; + transform: translate(-50%, -50%); + } + + .MuiSlider-thumb.MuiSlider-thumbColorPrimary:hover, + .MuiSlider-thumb.MuiSlider-thumbColorPrimary.Mui-focusVisible { + box-shadow: 0px 0px 0px 8px rgba(var(--mui-palette-primary-mainChannel) / 0.16); + } + + .MuiSlider-thumb.MuiSlider-thumbColorPrimary.Mui-active { + box-shadow: 0px 0px 0px 14px rgba(var(--mui-palette-primary-mainChannel) / 0.16); + } + + .MuiSlider-thumb.MuiSlider-thumbColorSecondary:hover, + .MuiSlider-thumb.MuiSlider-thumbColorSecondary.Mui-focusVisible { + box-shadow: 0px 0px 0px 8px rgba(var(--mui-palette-secondary-mainChannel) / 0.16); + } + + .MuiSlider-thumb.MuiSlider-thumbColorSecondary.Mui-active { + box-shadow: 0px 0px 0px 14px rgba(var(--mui-palette-secondary-mainChannel) / 0.16); + } + + .MuiSlider-thumb.MuiSlider-thumbColorError:hover, + .MuiSlider-thumb.MuiSlider-thumbColorError.Mui-focusVisible { + box-shadow: 0px 0px 0px 8px rgba(var(--mui-palette-error-mainChannel) / 0.16); + } + + .MuiSlider-thumb.MuiSlider-thumbColorError.Mui-active { + box-shadow: 0px 0px 0px 14px rgba(var(--mui-palette-error-mainChannel) / 0.16); + } + + .MuiSlider-thumb.MuiSlider-thumbColorInfo:hover, + .MuiSlider-thumb.MuiSlider-thumbColorInfo.Mui-focusVisible { + box-shadow: 0px 0px 0px 8px rgba(var(--mui-palette-info-mainChannel) / 0.16); + } + + .MuiSlider-thumb.MuiSlider-thumbColorInfo.Mui-active { + box-shadow: 0px 0px 0px 14px rgba(var(--mui-palette-info-mainChannel) / 0.16); + } + + .MuiSlider-thumb.MuiSlider-thumbColorSuccess:hover, + .MuiSlider-thumb.MuiSlider-thumbColorSuccess.Mui-focusVisible { + box-shadow: 0px 0px 0px 8px rgba(var(--mui-palette-success-mainChannel) / 0.16); + } + + .MuiSlider-thumb.MuiSlider-thumbColorSuccess.Mui-active { + box-shadow: 0px 0px 0px 14px rgba(var(--mui-palette-success-mainChannel) / 0.16); + } + + .MuiSlider-thumb.MuiSlider-thumbColorWarning:hover, + .MuiSlider-thumb.MuiSlider-thumbColorWarning.Mui-focusVisible { + box-shadow: 0px 0px 0px 8px rgba(var(--mui-palette-warning-mainChannel) / 0.16); + } + + .MuiSlider-thumb.MuiSlider-thumbColorWarning.Mui-active { + box-shadow: 0px 0px 0px 14px rgba(var(--mui-palette-warning-mainChannel) / 0.16); + } + + @media (hover: none) { + .MuiSlider-thumb.MuiSlider-thumbColorPrimary:hover, + .MuiSlider-thumb.MuiSlider-thumbColorSecondary:hover, + .MuiSlider-thumb.MuiSlider-thumbColorError:hover, + .MuiSlider-thumb.MuiSlider-thumbColorInfo:hover, + .MuiSlider-thumb.MuiSlider-thumbColorSuccess:hover, + .MuiSlider-thumb.MuiSlider-thumbColorWarning:hover { + box-shadow: none; + } + } + + .MuiSlider-thumb.MuiSlider-thumbSizeSmall { + width: 12px; + height: 12px; + } + + .MuiSlider-thumb.MuiSlider-thumbSizeSmall::before { + box-shadow: none; + } + + .MuiSlider-thumb.MuiSlider-horizontal { + inset-block-start: 50%; + transform: translate(-50%, -50%); + } + + [dir='rtl'] .MuiSlider-thumb.MuiSlider-horizontal { + transform: translate(50%, -50%); + } + + .MuiSlider-thumb.MuiSlider-vertical { + inset-inline-start: 50%; + transform: translate(-50%, 50%); + } + + .MuiSlider-thumb.Mui-disabled:hover { + box-shadow: none; + } + + .MuiSlider-valueLabel { + z-index: 1; + white-space: nowrap; + font-size: 0.875rem; + line-height: 1.43; + letter-spacing: 0.01071em; + font-weight: 500; + transition: transform var(--mui-transitions-duration-shortest) + var(--mui-transitions-easing-easeInOut); + position: absolute; + background-color: var(--mui-palette-grey-600); + border-radius: 2px; + color: var(--mui-palette-common-white); + display: flex; + align-items: center; + justify-content: center; + padding: 0.25rem 0.75rem; + } + + .MuiSlider-valueLabel.MuiSlider-horizontal { + transform: translateY(-100%) scale(0); + inset-block-start: -10px; + transform-origin: block-end center; + } + + .MuiSlider-valueLabel.MuiSlider-horizontal::before { + position: absolute; + content: ''; + width: 8px; + height: 8px; + transform: translate(-50%, 50%) rotate(45deg); + background-color: inherit; + inset-block-end: 0; + inset-inline-start: 50%; + } + + .MuiSlider-valueLabel.MuiSlider-horizontal.MuiSlider-valueLabelOpen { + transform: translateY(-100%) scale(1); + } + + .MuiSlider-valueLabel.MuiSlider-vertical { + transform: translateY(-50%) scale(0); + inset-inline-end: 30px; + inset-block-start: 50%; + transform-origin: inline-end center; + } + + .MuiSlider-valueLabel.MuiSlider-vertical::before { + position: absolute; + content: ''; + width: 8px; + height: 8px; + transform: translate(-50%, -50%) rotate(45deg); + background-color: inherit; + inset-inline-end: -8px; + inset-block-start: 50%; + } + + .MuiSlider-valueLabel.MuiSlider-vertical.MuiSlider-valueLabelOpen { + transform: translateY(-50%) scale(1); + } + + .MuiSlider-valueLabel.MuiSlider-sizeSmall { + font-size: 0.75rem; + padding: 0.25rem 0.5rem; + } + + .MuiSlider-valueLabel.MuiSlider-vertical.MuiSlider-sizeSmall { + inset-inline-end: 20px; + } + + .MuiSlider-mark { + position: absolute; + width: 2px; + height: 2px; + border-radius: 1px; + background-color: currentColor; + } + + .MuiSlider-mark.MuiSlider-horizontal { + inset-block-start: 50%; + transform: translate(-1px, -50%); + } + + [dir='rtl'] .MuiSlider-mark.MuiSlider-horizontal { + transform: translate(1px, -50%); + } + + .MuiSlider-mark.MuiSlider-vertical { + inset-inline-start: 50%; + transform: translate(-50%, 1px); + } + + .MuiSlider-mark.MuiSlider-markActive { + background-color: var(--mui-palette-background-paper); + opacity: 0.8; + } + + .MuiSlider-markLabel { + font-size: 0.875rem; + line-height: 1.43; + letter-spacing: 0.01071em; + color: var(--mui-palette-text-secondary); + position: absolute; + white-space: nowrap; + } + + .MuiSlider-markLabel.MuiSlider-horizontal { + inset-block-start: 30px; + transform: translateX(-50%); + } + + [dir='rtl'] .MuiSlider-markLabel.MuiSlider-horizontal { + transform: translateX(50%); + } + + @media (pointer: coarse) { + .MuiSlider-markLabel.MuiSlider-horizontal { + inset-block-start: 40px; + } + } + + .MuiSlider-markLabel.MuiSlider-vertical { + inset-inline-start: 36px; + transform: translateY(50%); + } + + @media (pointer: coarse) { + .MuiSlider-markLabel.MuiSlider-vertical { + inset-inline-start: 44px; + } + } + + .MuiSlider-markLabel.MuiSlider-markLabelActive { + color: var(--mui-palette-text-primary); + } +} diff --git a/packages/mui-material/src/styles/shouldSkipGeneratingVar.ts b/packages/mui-material/src/styles/shouldSkipGeneratingVar.ts index 53902a5f4476dc..e2ecb63c904b9d 100644 --- a/packages/mui-material/src/styles/shouldSkipGeneratingVar.ts +++ b/packages/mui-material/src/styles/shouldSkipGeneratingVar.ts @@ -2,7 +2,7 @@ export default function shouldSkipGeneratingVar(keys: string[]) { return ( keys[0] === 'motion' || !!keys[0].match( - /(cssVarPrefix|colorSchemeSelector|modularCssLayers|rootSelector|typography|mixins|breakpoints|direction|transitions)/, + /(cssVarPrefix|colorSchemeSelector|modularCssLayers|rootSelector|typography|mixins|breakpoints|direction)/, ) || !!keys[0].match(/sxConfig$/) || // ends with sxConfig (keys[0] === 'palette' && !!keys[1]?.match(/(mode|contrastThreshold|tonalOffset)/)) diff --git a/packages/mui-system/src/cssVars/cssVarsParser.ts b/packages/mui-system/src/cssVars/cssVarsParser.ts index c1876b18f1abc1..2f8ef8655fa092 100644 --- a/packages/mui-system/src/cssVars/cssVarsParser.ts +++ b/packages/mui-system/src/cssVars/cssVarsParser.ts @@ -87,6 +87,10 @@ const getCssValue = (keys: string[], value: string | number) => { // CSS property that are unitless return value; } + if (keys[0] === 'transitions' && keys[1] === 'duration') { + // transition duration is in ms, unit is required + return `${value}ms`; + } const lastKey = keys[keys.length - 1]; if (lastKey.toLowerCase().includes('opacity')) { // opacity values are unitless diff --git a/packages/mui-utils/src/composeClasses/composeClasses.ts b/packages/mui-utils/src/composeClasses/composeClasses.ts index 0a1639a6cf5ace..89a65f59ccfeaf 100644 --- a/packages/mui-utils/src/composeClasses/composeClasses.ts +++ b/packages/mui-utils/src/composeClasses/composeClasses.ts @@ -29,12 +29,14 @@ * @param slots a list of classes for each possible slot * @param getUtilityClass a function to resolve the class based on the slot name * @param classes the input classes from props + * @param cssModules the CSS module classes to apply * @returns the resolved classes for all slots */ export default function composeClasses( slots: Record>, getUtilityClass: (slot: string) => string, classes: Record | undefined = undefined, + cssModules: Record | undefined = undefined, ): Record { const output: Record = {} as any; @@ -46,12 +48,17 @@ export default function composeClasses( for (let i = 0; i < slot.length; i += 1) { const value = slot[i]; if (value) { - buffer += (start === true ? '' : ' ') + getUtilityClass(value); + const utilityClass = getUtilityClass(value); + buffer += (start === true ? '' : ' ') + utilityClass; start = false; if (classes && classes[value]) { buffer += ' ' + classes[value]; } + + if (cssModules && cssModules[utilityClass]) { + buffer += ' ' + cssModules[utilityClass]; + } } } From e85c24b787216ba9ca058854705d2aecc8b4c92f Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Mon, 15 Jun 2026 14:25:55 +0300 Subject: [PATCH 06/21] use nbsp chars in tailwind readme --- packages/mui-tailwind/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mui-tailwind/README.md b/packages/mui-tailwind/README.md index 1b951636a3ad37..0b202778880f21 100644 --- a/packages/mui-tailwind/README.md +++ b/packages/mui-tailwind/README.md @@ -1,6 +1,6 @@ # @mui/tailwind -Tailwind CSS preset, plugin, and v4 CSS file for Material UI. +Tailwind CSS preset, plugin, and v4 CSS file for Material UI. Full documentation, setup guide, and demos are in [`docs/data/material/integrations/tailwindcss/tailwindcss-components.md`](../../docs/data/material/integrations/tailwindcss/tailwindcss-components.md). From b7d7b388edb6f84fe6810e2dbe5ce0ef3c616eaf Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Mon, 15 Jun 2026 15:45:50 +0300 Subject: [PATCH 07/21] playground with slider + emotion --- docs/pages/experiments/emotion-slider.tsx | 251 ++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 docs/pages/experiments/emotion-slider.tsx diff --git a/docs/pages/experiments/emotion-slider.tsx b/docs/pages/experiments/emotion-slider.tsx new file mode 100644 index 00000000000000..13baee2ea60910 --- /dev/null +++ b/docs/pages/experiments/emotion-slider.tsx @@ -0,0 +1,251 @@ +'use client'; +import * as React from 'react'; +import { createTheme, ThemeProvider, styled, useColorScheme } from '@mui/material/styles'; +import Box from '@mui/material/Box'; +import Slider from '@mui/material/Slider'; +import Stack from '@mui/material/Stack'; +import Switch from '@mui/material/Switch'; +import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; +import Typography from '@mui/material/Typography'; + +// ---------- Themes ---------------------------------------------------------- + +const themes = { + violet: createTheme({ + cssVariables: { colorSchemeSelector: 'data' }, + colorSchemes: { + light: { palette: { primary: { main: '#7c3aed' }, secondary: { main: '#db2777' } } }, + dark: { palette: { primary: { main: '#a78bfa' }, secondary: { main: '#f472b6' } } }, + }, + shape: { borderRadius: 12 }, + }), + teal: createTheme({ + cssVariables: { colorSchemeSelector: 'data' }, + colorSchemes: { + light: { palette: { primary: { main: '#0d9488' }, secondary: { main: '#f59e0b' } } }, + dark: { palette: { primary: { main: '#2dd4bf' }, secondary: { main: '#fbbf24' } } }, + }, + shape: { borderRadius: 4 }, + components: { + MuiSlider: { + styleOverrides: { + thumb: { width: 24, height: 24 }, + track: { height: 6 }, + rail: { height: 6 }, + }, + }, + }, + }), + blue: createTheme({ + cssVariables: { colorSchemeSelector: 'data' }, + colorSchemes: { + light: { palette: { primary: { main: '#1d4ed8' }, secondary: { main: '#dc2626' } } }, + dark: { palette: { primary: { main: '#60a5fa' }, secondary: { main: '#f87171' } } }, + }, + shape: { borderRadius: 8 }, + components: { + MuiSlider: { + defaultProps: { size: 'small' }, + }, + }, + }), +} as const; + +type ThemeKey = keyof typeof themes; + +// ---------- styled() example ------------------------------------------------ + +const GradientSlider = styled(Slider)(({ theme }) => ({ + '& .MuiSlider-track': { + background: `linear-gradient(90deg, ${theme.palette.primary.main}, ${theme.palette.secondary.main})`, + border: 'none', + }, + '& .MuiSlider-thumb': { + background: theme.palette.secondary.main, + }, +})); + +// ---------- Layout helpers -------------------------------------------------- + +function Section({ label, children }: { label: string; children: React.ReactNode }) { + return ( + + + {label} + + {children} + + ); +} + +// ---------- useMounted guard (avoid SSR hydration mismatch) ---------------- + +function useMounted() { + const [mounted, setMounted] = React.useState(false); + React.useEffect(() => setMounted(true), []); + return mounted; +} + +// ---------- Page ------------------------------------------------------------ + +function Page({ + themeKey, + setThemeKey, +}: { + themeKey: ThemeKey; + setThemeKey: (key: ThemeKey) => void; +}) { + const { mode, setMode } = useColorScheme(); + const mounted = useMounted(); + const [value1, setValue1] = React.useState(40); + const [value2, setValue2] = React.useState(60); + const [value3, setValue3] = React.useState(30); + const [value4, setValue4] = React.useState([20, 70]); + + return ( + + {/* Header */} + + Emotion + Slider + + + Verifies Slider still works with Emotion customization: sx,{' '} + styled(), styleOverrides, and defaultProps via{' '} + createTheme. + + + {/* ── Theme + dark mode switcher ──────────────────────────────────── */} +
+ + v && setThemeKey(v as ThemeKey)} + size="small" + > + {(Object.keys(themes) as ThemeKey[]).map((key) => ( + + {key} + + ))} + + + Light + setMode(event.target.checked ? 'dark' : 'light')} + /> + Dark + + + + {themeKey === 'teal' && + 'This theme overrides MuiSlider — larger thumb & track via styleOverrides.'} + {themeKey === 'blue' && 'This theme sets size="small" on all Sliders via defaultProps.'} + {themeKey === 'violet' && 'Default theme with rounded corners (borderRadius: 12).'} + +
+ + {/* ── Default ─────────────────────────────────────────────────────── */} +
+ setValue1(v as number)} /> + + Value: {value1} + +
+ + {/* ── sx prop ─────────────────────────────────────────────────────── */} +
+ setValue2(v as number)} + sx={{ + color: 'secondary.main', + '& .MuiSlider-thumb': { + width: 22, + height: 22, + boxShadow: (t) => `0 0 0 7px ${t.palette.secondary.main}28`, + }, + '& .MuiSlider-rail': { opacity: 0.3 }, + }} + /> + + Value: {value2} + +
+ + {/* ── styled() ────────────────────────────────────────────────────── */} +
+ setValue3(v as number)} /> + + Value: {value3} + +
+ + {/* ── Range slider ────────────────────────────────────────────────── */} +
+ setValue4(v as number[])} + marks + step={10} + valueLabelDisplay="auto" + sx={{ '& .MuiSlider-markLabel': { fontSize: '0.7rem' } }} + /> + + Value: [{value4.join(', ')}] + +
+ + {/* ── Disabled ────────────────────────────────────────────────────── */} +
+ +
+ + {/* ── Vertical ────────────────────────────────────────────────────── */} +
+ + setValue1(v as number)} + /> + setValue2(v as number)} + sx={{ color: 'secondary.main' }} + /> + +
+
+ ); +} + +export default function EmotionSlider() { + const [themeKey, setThemeKey] = React.useState('violet'); + return ( + + + + ); +} From 3e54e5640acea79f7e8163553f55aa64c48ef483 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Mon, 15 Jun 2026 16:21:08 +0300 Subject: [PATCH 08/21] add the styling noop package --- packages/mui-styled-engine-noop/README.md | 12 ++++ packages/mui-styled-engine-noop/package.json | 58 +++++++++++++++++ .../src/GlobalStyles/GlobalStyles.d.ts | 11 ++++ .../src/GlobalStyles/GlobalStyles.js | 5 ++ .../src/GlobalStyles/index.d.ts | 2 + .../src/GlobalStyles/index.js | 1 + .../StyledEngineProvider.d.ts | 8 +++ .../StyledEngineProvider.js | 6 ++ .../src/StyledEngineProvider/index.d.ts | 2 + .../src/StyledEngineProvider/index.js | 1 + .../mui-styled-engine-noop/src/index.d.ts | 54 ++++++++++++++++ packages/mui-styled-engine-noop/src/index.js | 62 +++++++++++++++++++ packages/mui-styled-engine-noop/tsconfig.json | 4 ++ 13 files changed, 226 insertions(+) create mode 100644 packages/mui-styled-engine-noop/README.md create mode 100644 packages/mui-styled-engine-noop/package.json create mode 100644 packages/mui-styled-engine-noop/src/GlobalStyles/GlobalStyles.d.ts create mode 100644 packages/mui-styled-engine-noop/src/GlobalStyles/GlobalStyles.js create mode 100644 packages/mui-styled-engine-noop/src/GlobalStyles/index.d.ts create mode 100644 packages/mui-styled-engine-noop/src/GlobalStyles/index.js create mode 100644 packages/mui-styled-engine-noop/src/StyledEngineProvider/StyledEngineProvider.d.ts create mode 100644 packages/mui-styled-engine-noop/src/StyledEngineProvider/StyledEngineProvider.js create mode 100644 packages/mui-styled-engine-noop/src/StyledEngineProvider/index.d.ts create mode 100644 packages/mui-styled-engine-noop/src/StyledEngineProvider/index.js create mode 100644 packages/mui-styled-engine-noop/src/index.d.ts create mode 100644 packages/mui-styled-engine-noop/src/index.js create mode 100644 packages/mui-styled-engine-noop/tsconfig.json diff --git a/packages/mui-styled-engine-noop/README.md b/packages/mui-styled-engine-noop/README.md new file mode 100644 index 00000000000000..35784e10d7d44f --- /dev/null +++ b/packages/mui-styled-engine-noop/README.md @@ -0,0 +1,12 @@ +# @mui/styled-engine-noop + +This package is a wrapper around the `@emotion/react` package. +It also provides a shared interface that can be used with other styled engines, +like styled-components. +It is used internally in the `@mui/system` package. + +## Documentation + + + +Visit [https://mui.com/material-ui/integrations/styled-components/](https://mui.com/material-ui/integrations/styled-components/) to view the full documentation. diff --git a/packages/mui-styled-engine-noop/package.json b/packages/mui-styled-engine-noop/package.json new file mode 100644 index 00000000000000..2e55de8d98ee77 --- /dev/null +++ b/packages/mui-styled-engine-noop/package.json @@ -0,0 +1,58 @@ +{ + "name": "@mui/styled-engine-noop", + "version": "10.0.0-alpha.0", + "author": "MUI Team", + "description": "styled() API noop package for interoperability with CSS in JS libraries.", + "license": "MIT", + "keywords": [ + "react", + "react-component", + "mui", + "emotion" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/mui/material-ui.git", + "directory": "packages/mui-styled-engine-noop" + }, + "bugs": { + "url": "https://github.com/mui/material-ui/issues" + }, + "homepage": "https://mui.com/system/styled/", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "scripts": { + "build": "code-infra build --flat --skipTsc", + "release": "pnpm build && pnpm publish", + "test": "pnpm --workspace-root test:unit --project \"*:@mui/styled-engine-noop\"", + "typescript": "tsc -p tsconfig.json" + }, + "dependencies": { + "@babel/runtime": "^7.29.2", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "devDependencies": { + "@types/chai": "5.2.3", + "@types/react": "19.2.14", + "@emotion/react": "11.14.0", + "chai": "6.2.2", + "react": "19.2.4" + }, + "peerDependencies": {}, + "peerDependenciesMeta": {}, + "sideEffects": false, + "publishConfig": { + "access": "public", + "directory": "build" + }, + "engines": { + "node": ">=14.0.0" + }, + "exports": { + ".": "./src/index.js", + "./*": "./src/*/index.js" + } +} diff --git a/packages/mui-styled-engine-noop/src/GlobalStyles/GlobalStyles.d.ts b/packages/mui-styled-engine-noop/src/GlobalStyles/GlobalStyles.d.ts new file mode 100644 index 00000000000000..0d6af5c9d1d5f6 --- /dev/null +++ b/packages/mui-styled-engine-noop/src/GlobalStyles/GlobalStyles.d.ts @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { Interpolation } from '@emotion/react'; + +export interface GlobalStylesProps { + defaultTheme?: object | undefined; + styles: Interpolation; +} + +export default function GlobalStyles( + props: GlobalStylesProps, +): React.JSX.Element; diff --git a/packages/mui-styled-engine-noop/src/GlobalStyles/GlobalStyles.js b/packages/mui-styled-engine-noop/src/GlobalStyles/GlobalStyles.js new file mode 100644 index 00000000000000..2df92571bb9331 --- /dev/null +++ b/packages/mui-styled-engine-noop/src/GlobalStyles/GlobalStyles.js @@ -0,0 +1,5 @@ +'use client'; + +export default function GlobalStyles() { + return null; +} diff --git a/packages/mui-styled-engine-noop/src/GlobalStyles/index.d.ts b/packages/mui-styled-engine-noop/src/GlobalStyles/index.d.ts new file mode 100644 index 00000000000000..a815f5c4a572b2 --- /dev/null +++ b/packages/mui-styled-engine-noop/src/GlobalStyles/index.d.ts @@ -0,0 +1,2 @@ +export { default } from './GlobalStyles'; +export * from './GlobalStyles'; diff --git a/packages/mui-styled-engine-noop/src/GlobalStyles/index.js b/packages/mui-styled-engine-noop/src/GlobalStyles/index.js new file mode 100644 index 00000000000000..48e20189092434 --- /dev/null +++ b/packages/mui-styled-engine-noop/src/GlobalStyles/index.js @@ -0,0 +1 @@ +export { default } from './GlobalStyles'; diff --git a/packages/mui-styled-engine-noop/src/StyledEngineProvider/StyledEngineProvider.d.ts b/packages/mui-styled-engine-noop/src/StyledEngineProvider/StyledEngineProvider.d.ts new file mode 100644 index 00000000000000..f62068b3bc0ab7 --- /dev/null +++ b/packages/mui-styled-engine-noop/src/StyledEngineProvider/StyledEngineProvider.d.ts @@ -0,0 +1,8 @@ +import * as React from 'react'; + +export interface StyledEngineProviderProps { + children?: React.ReactNode; + injectFirst?: boolean | undefined; +} + +export default function StyledEngineProvider(props: StyledEngineProviderProps): React.JSX.Element; diff --git a/packages/mui-styled-engine-noop/src/StyledEngineProvider/StyledEngineProvider.js b/packages/mui-styled-engine-noop/src/StyledEngineProvider/StyledEngineProvider.js new file mode 100644 index 00000000000000..c4f5375f7760da --- /dev/null +++ b/packages/mui-styled-engine-noop/src/StyledEngineProvider/StyledEngineProvider.js @@ -0,0 +1,6 @@ +'use client'; + +export default function StyledEngineProvider(props) { + const { children } = props; + return children; +} diff --git a/packages/mui-styled-engine-noop/src/StyledEngineProvider/index.d.ts b/packages/mui-styled-engine-noop/src/StyledEngineProvider/index.d.ts new file mode 100644 index 00000000000000..7fdd272b28e6ca --- /dev/null +++ b/packages/mui-styled-engine-noop/src/StyledEngineProvider/index.d.ts @@ -0,0 +1,2 @@ +export { default } from './StyledEngineProvider'; +export * from './StyledEngineProvider'; diff --git a/packages/mui-styled-engine-noop/src/StyledEngineProvider/index.js b/packages/mui-styled-engine-noop/src/StyledEngineProvider/index.js new file mode 100644 index 00000000000000..30cd6986a545fa --- /dev/null +++ b/packages/mui-styled-engine-noop/src/StyledEngineProvider/index.js @@ -0,0 +1 @@ +export { default } from './StyledEngineProvider'; diff --git a/packages/mui-styled-engine-noop/src/index.d.ts b/packages/mui-styled-engine-noop/src/index.d.ts new file mode 100644 index 00000000000000..bf231894c8437c --- /dev/null +++ b/packages/mui-styled-engine-noop/src/index.d.ts @@ -0,0 +1,54 @@ +export { default as StyledEngineProvider } from './StyledEngineProvider'; + +export { default as GlobalStyles } from './GlobalStyles'; +export * from './GlobalStyles'; + +export declare const ThemeContext: React.Context; +export function keyframes(arg: any): string; +export function css(arg: any): string; + +/** + * For internal usage in `@mui/system` package + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export function internal_mutateStyles( + tag: React.ElementType, + processor: (styles: any) => any, +): void; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export function internal_serializeStyles(styles: any): object; + +export interface SerializedStyles { + name: string; + styles: string; + map?: string | undefined; + next?: SerializedStyles | undefined; +} + +export type Keyframes = { + name: string; + styles: string; + anim: number; + toString: () => string; +} & string; + +export function shouldForwardProp(propName: PropertyKey): boolean; + +/** Same as StyledOptions but shouldForwardProp must be a type guard */ +export interface FilteringStyledOptions { + label?: string | undefined; + shouldForwardProp?(propName: PropertyKey): propName is ForwardedProps; + target?: string | undefined; +} + +export interface CSSObject {} + +export type Interpolation = any; + +export interface CreateMUIStyled { + ( + component: ComponentType, + options: object, + ): React.ComponentType; +} diff --git a/packages/mui-styled-engine-noop/src/index.js b/packages/mui-styled-engine-noop/src/index.js new file mode 100644 index 00000000000000..d43bc0d3787094 --- /dev/null +++ b/packages/mui-styled-engine-noop/src/index.js @@ -0,0 +1,62 @@ +'use client'; +import * as React from 'react'; +import PropTypes from 'prop-types'; + +const defaultShouldForwardProp = (prop) => + prop !== 'ownerState' && prop !== 'theme' && prop !== 'sx' && prop !== 'as'; + +export default function styled(Tag, options) { + const { shouldForwardProp } = options || {}; + const finalShouldForwardProp = shouldForwardProp || defaultShouldForwardProp; + + let warnedAboutSx = false; + + const Component = React.forwardRef(function StyledComponent(props, ref) { + if (process.env.NODE_ENV !== 'production' && !warnedAboutSx && props.sx !== undefined) { + warnedAboutSx = true; + const componentName = + typeof Tag === 'string' ? `<${Tag}>` : Tag.displayName || Tag.name || 'a component'; + console.error( + `MUI: The \`sx\` prop was used on ${componentName} but \`@mui/styled-engine-noop\` is active. ` + + 'The `sx` prop will be ignored. Use Tailwind CSS classes via the `className` prop for styling instead.', + ); + } + + const { as: asProp, ...restProps } = props; + const FinalTag = asProp || Tag; + + const forwardedProps = Object.fromEntries( + Object.entries(restProps).filter(([prop]) => finalShouldForwardProp(prop)), + ); + + return ; + }); + + Component.propTypes = { + as: PropTypes.elementType, + sx: PropTypes.oneOfType([PropTypes.array, PropTypes.func, PropTypes.object]), + }; + + return () => Component; +} + +export function keyframes() { + return 'animation-name'; +} + +export function css() { + return ''; +} + +export const ThemeContext = React.createContext(null); + +// eslint-disable-next-line @typescript-eslint/naming-convention +export function internal_mutateStyles(_tag, _processor) {} + +// eslint-disable-next-line @typescript-eslint/naming-convention +export function internal_serializeStyles(_styles) { + return ''; +} + +export { default as StyledEngineProvider } from './StyledEngineProvider'; +export { default as GlobalStyles } from './GlobalStyles'; diff --git a/packages/mui-styled-engine-noop/tsconfig.json b/packages/mui-styled-engine-noop/tsconfig.json new file mode 100644 index 00000000000000..52d43eaaa9b975 --- /dev/null +++ b/packages/mui-styled-engine-noop/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*"] +} From a37019ec5e7c0a652c17c30610927a0bae474603 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Mon, 15 Jun 2026 17:28:45 +0300 Subject: [PATCH 09/21] CssThemeProvider --- .../src/styles/CssThemeProvider.test.tsx | 74 +++++++++ .../src/styles/CssThemeProvider.tsx | 101 ++++++++++++ .../src/styles/CssVarsInjector.test.tsx | 147 ++++++++++++++++++ .../src/styles/CssVarsInjector.tsx | 75 +++++++++ packages/mui-material/src/styles/index.d.ts | 1 + packages/mui-material/src/styles/index.js | 1 + packages/mui-system/src/cssVars/index.ts | 1 + .../src/cssVars/styleSheetsToString.test.ts | 126 +++++++++++++++ .../src/cssVars/styleSheetsToString.ts | 65 ++++++++ packages/mui-system/src/index.d.ts | 1 + packages/mui-system/src/index.js | 1 + pnpm-lock.yaml | 67 +++++++- 12 files changed, 654 insertions(+), 6 deletions(-) create mode 100644 packages/mui-material/src/styles/CssThemeProvider.test.tsx create mode 100644 packages/mui-material/src/styles/CssThemeProvider.tsx create mode 100644 packages/mui-material/src/styles/CssVarsInjector.test.tsx create mode 100644 packages/mui-material/src/styles/CssVarsInjector.tsx create mode 100644 packages/mui-system/src/cssVars/styleSheetsToString.test.ts create mode 100644 packages/mui-system/src/cssVars/styleSheetsToString.ts diff --git a/packages/mui-material/src/styles/CssThemeProvider.test.tsx b/packages/mui-material/src/styles/CssThemeProvider.test.tsx new file mode 100644 index 00000000000000..5638e1d302fe07 --- /dev/null +++ b/packages/mui-material/src/styles/CssThemeProvider.test.tsx @@ -0,0 +1,74 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { createRenderer } from '@mui/internal-test-utils'; +import { createTheme } from '@mui/material/styles'; +import CssThemeProvider from './CssThemeProvider'; +import useTheme from './useTheme'; + +describe('CssThemeProvider', () => { + const { render } = createRenderer(); + + afterEach(() => { + document.querySelectorAll('[id$="-css-vars"]').forEach((el) => el.remove()); + }); + + it('throws when the theme was not created with cssVariables: true', () => { + const theme = createTheme(); + expect(() => render({null})).to.throw( + 'MUI: CssThemeProvider requires a theme created with `cssVariables: true`', + ); + }); + + it('throws when passed a theme function', () => { + const themeFn = () => createTheme({ cssVariables: true }); + expect(() => render({null})).to.throw( + 'MUI: CssThemeProvider does not accept a theme function', + ); + }); + + it('injects CSS variables into ', () => { + const theme = createTheme({ cssVariables: true }); + render( + +
+ , + ); + const style = document.getElementById('mui-css-vars'); + expect(style).not.to.equal(null); + expect(style!.textContent).to.include('--mui-palette-primary-main'); + }); + + it('makes the theme available via useTheme()', () => { + const theme = createTheme({ + cssVariables: true, + palette: { primary: { main: '#ff0000' } }, + }); + let observed: any; + function Probe() { + observed = useTheme(); + return null; + } + render( + + + , + ); + // Theme is wrapped in CSS-vars mode — palette values are var() strings, + // but transitions/breakpoints/spacing/etc. remain plain JS values. + expect(observed).not.to.equal(undefined); + expect(observed.breakpoints).to.be.an('object'); + expect(observed.spacing(2)).to.equal('16px'); + expect(observed.transitions.duration.shortest).to.equal(150); + }); + + it('uses the cssVarPrefix to namespace the injected +
+
+ ); +} diff --git a/test/css-theme-provider-vite-sandbox/src/main.tsx b/test/css-theme-provider-vite-sandbox/src/main.tsx new file mode 100644 index 00000000000000..033af594370fdc --- /dev/null +++ b/test/css-theme-provider-vite-sandbox/src/main.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import * as ReactDOMClient from 'react-dom/client'; +import App from './App'; + +const container = document.getElementById('root'); +if (!container) { + throw new Error('Missing #root element'); +} + +ReactDOMClient.createRoot(container).render( + + + , +); diff --git a/test/css-theme-provider-vite-sandbox/src/vite-env.d.ts b/test/css-theme-provider-vite-sandbox/src/vite-env.d.ts new file mode 100644 index 00000000000000..11f02fe2a0061d --- /dev/null +++ b/test/css-theme-provider-vite-sandbox/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/test/css-theme-provider-vite-sandbox/tsconfig.json b/test/css-theme-provider-vite-sandbox/tsconfig.json new file mode 100644 index 00000000000000..7b1f33c4f6a88a --- /dev/null +++ b/test/css-theme-provider-vite-sandbox/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "vite.config.ts"] +} diff --git a/test/css-theme-provider-vite-sandbox/vite.config.ts b/test/css-theme-provider-vite-sandbox/vite.config.ts new file mode 100644 index 00000000000000..10052c6ef5282e --- /dev/null +++ b/test/css-theme-provider-vite-sandbox/vite.config.ts @@ -0,0 +1,76 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { defineConfig, transformWithEsbuild } from 'vite'; +import react from '@vitejs/plugin-react'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); +// Two levels up: test/css-theme-provider-vite-sandbox → test → monorepo root +const MONOREPO_ROOT = path.resolve(dirname, '../..'); + +// https://vite.dev/config/ +// Use the function form so `mode` is available for the NODE_ENV define. +export default defineConfig(({ mode }) => ({ + // MUI source files reference process.env.NODE_ENV for dev-only warnings. + // Vite doesn't polyfill `process` in browser builds, so we define it explicitly. + define: { + 'process.env.NODE_ENV': JSON.stringify(mode === 'production' ? 'production' : 'development'), + }, + optimizeDeps: { + esbuildOptions: { + // MUI source files use .js extension but contain JSX syntax. + // Tell esbuild's pre-bundler to treat them as JSX. + loader: { '.js': 'jsx' }, + }, + }, + plugins: [ + // @mui/material source files use .js extension but contain JSX. + // This plugin re-parses them as JSX so Rollup/esbuild can handle them. + // enforce: 'pre' ensures this plugin runs before Vite's internal esbuild + // pre-transform, which would otherwise reject JSX in .js files. + { + name: 'treat-js-files-as-jsx', + enforce: 'pre' as const, + transform(code, id) { + if (/\/node_modules\//.test(id)) { + return null; + } + if (id.startsWith('\0')) { + return null; + } + if (!/.*\.js$/.test(id)) { + return null; + } + return transformWithEsbuild(code, id, { loader: 'jsx' }); + }, + }, + react(), + ], + resolve: { + alias: [ + // Resolve all @mui/* packages from monorepo source so we test the branch + // code, not a published release. + { + find: '@mui/material', + replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-material/src'), + }, + { + find: '@mui/system', + replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-system/src'), + }, + { + find: '@mui/utils', + replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-utils/src'), + }, + { + find: '@mui/private-theming', + replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-private-theming/src'), + }, + // THE KEY ALIAS: swap Emotion for the zero-runtime noop engine. + // 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'), + }, + ], + }, +})); diff --git a/test/emotion-vite-sandbox/README.md b/test/emotion-vite-sandbox/README.md new file mode 100644 index 00000000000000..21aa48fe3ce1c6 --- /dev/null +++ b/test/emotion-vite-sandbox/README.md @@ -0,0 +1,74 @@ +# emotion-vite-sandbox + +A minimal Vite + React app that exercises the **Emotion (default) path** for +Material UI components. + +## Purpose + +Verifies that: + +1. `@mui/styled-engine` resolves to the real Emotion-backed engine (bundle + contains `@emotion/*`). +2. `Slider` renders correctly via `ThemeProvider` + Emotion-generated styles. +3. The `sx` prop works and applies styles at runtime via Emotion. +4. `className`-based overrides work alongside Emotion-generated class names. +5. Dark mode works via `ThemeProvider`'s `colorSchemes` / `CssVarsProvider` + by flipping `data-mui-color-scheme="dark"` on `document.documentElement`. + +### Why only Slider? + +Only `Slider` is used because it is the first component converted to the +CSS-base pattern. It serves as the comparison baseline against +`test/noop-vite-sandbox` — same component, same visual output, different +engine. Bundle size delta between the two sandboxes measures the Emotion +overhead. + +## Dev server + +```bash +pnpm -F @mui-internal/emotion-vite-sandbox dev +``` + +Then open . + +## Production build + +```bash +pnpm -F @mui-internal/emotion-vite-sandbox build +``` + +## Verifying Emotion is bundled + +After a production build, confirm `@emotion/*` runtime is present: + +```bash +# Should print matches +grep -r "@emotion" test/emotion-vite-sandbox/dist/ +``` + +## Bundle size comparison + +Build all three sandboxes, then compare JS output sizes to measure the overhead +of each approach: + +```bash +pnpm -F @mui-internal/noop-vite-sandbox build +pnpm -F @mui-internal/css-theme-provider-vite-sandbox build +pnpm -F @mui-internal/emotion-vite-sandbox build + +# Compare JS sizes +ls -lh test/noop-vite-sandbox/dist/assets/*.js +ls -lh test/css-theme-provider-vite-sandbox/dist/assets/*.js +ls -lh test/emotion-vite-sandbox/dist/assets/*.js + +# Compare CSS sizes +ls -lh test/noop-vite-sandbox/dist/assets/*.css +ls -lh test/css-theme-provider-vite-sandbox/dist/assets/*.css +ls -lh test/emotion-vite-sandbox/dist/assets/*.css +``` + +## Relation to the TODO + +This sandbox maps to **§8 (Bundle size validation)** in `STYLING_V8_TODO.md`. +It is the Emotion-path counterpart to `test/noop-vite-sandbox`, used to +measure the JS bundle delta between the two engines. diff --git a/test/emotion-vite-sandbox/index.html b/test/emotion-vite-sandbox/index.html new file mode 100644 index 00000000000000..c6217cd200816f --- /dev/null +++ b/test/emotion-vite-sandbox/index.html @@ -0,0 +1,12 @@ + + + + + + MUI emotion Vite sandbox + + +
+ + + diff --git a/test/emotion-vite-sandbox/package.json b/test/emotion-vite-sandbox/package.json new file mode 100644 index 00000000000000..93e2b152f820c0 --- /dev/null +++ b/test/emotion-vite-sandbox/package.json @@ -0,0 +1,28 @@ +{ + "name": "@mui-internal/emotion-vite-sandbox", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@mui/material": "workspace:*", + "@mui/system": "workspace:*", + "@mui/utils": "workspace:*", + "@mui/styled-engine": "workspace:*", + "react": "^19.2.6", + "react-dom": "^19.2.6" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^5.1.1", + "typescript": "^6.0.3", + "vite": "^7.3.1" + } +} diff --git a/test/emotion-vite-sandbox/src/App.tsx b/test/emotion-vite-sandbox/src/App.tsx new file mode 100644 index 00000000000000..7b7082dd31f5e5 --- /dev/null +++ b/test/emotion-vite-sandbox/src/App.tsx @@ -0,0 +1,99 @@ +/** + * emotion-engine sandbox — Slider only. + * + * Mirror of noop-vite-sandbox but using the Emotion engine (the default MUI + * setup). No engine alias is applied — @mui/styled-engine resolves to the + * real Emotion-backed package. + * + * Verifies that: + * 1. @mui/styled-engine resolves to the Emotion engine (bundle contains @emotion/*). + * 2. Slider renders correctly via ThemeProvider + Emotion-generated styles. + * 3. The `sx` prop works and applies styles at runtime via Emotion. + * 4. Dark mode works via ThemeProvider's colorSchemes / CssVarsProvider. + * 5. useTheme() returns live JS theme values. + * + * To confirm Emotion IS bundled, run: + * pnpm -F @mui-internal/emotion-vite-sandbox build + * Then: + * grep -r "@emotion" dist/ + * # should print matches + */ +import * as React from 'react'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; +import Slider from '@mui/material/Slider'; + +const theme = createTheme({ + cssVariables: { + colorSchemeSelector: '[data-mui-color-scheme="%s"]', + }, + colorSchemes: { light: true, dark: true }, +}); + +type Mode = 'light' | 'dark'; + +export default function App() { + const [value, setValue] = React.useState(40); + const [mode, setMode] = React.useState('light'); + + function toggleMode() { + const next: Mode = mode === 'light' ? 'dark' : 'light'; + document.documentElement.setAttribute('data-mui-color-scheme', next); + setMode(next); + } + + return ( + +
+

emotion-engine sandbox

+

+ Engine: @mui/styled-engine (Emotion) — Emotion is bundled here. +

+ + {/* Dark mode toggle */} + + +
+

Slider value: {value}

+ + {/* sx prop — works with Emotion, applies color override at runtime */} + setValue(v as number)} + sx={{ color: 'secondary.main' }} + aria-label="Sandbox slider" + /> + +

+ className override (thumb should be purple, overriding the default via plain CSS): +

+ + {/* className override */} + setValue(v as number)} + className="custom-slider" + aria-label="Custom class slider" + /> +
+ + +
+
+ ); +} diff --git a/test/emotion-vite-sandbox/src/main.tsx b/test/emotion-vite-sandbox/src/main.tsx new file mode 100644 index 00000000000000..033af594370fdc --- /dev/null +++ b/test/emotion-vite-sandbox/src/main.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import * as ReactDOMClient from 'react-dom/client'; +import App from './App'; + +const container = document.getElementById('root'); +if (!container) { + throw new Error('Missing #root element'); +} + +ReactDOMClient.createRoot(container).render( + + + , +); diff --git a/test/emotion-vite-sandbox/src/vite-env.d.ts b/test/emotion-vite-sandbox/src/vite-env.d.ts new file mode 100644 index 00000000000000..11f02fe2a0061d --- /dev/null +++ b/test/emotion-vite-sandbox/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/test/emotion-vite-sandbox/tsconfig.json b/test/emotion-vite-sandbox/tsconfig.json new file mode 100644 index 00000000000000..7b1f33c4f6a88a --- /dev/null +++ b/test/emotion-vite-sandbox/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "vite.config.ts"] +} diff --git a/test/emotion-vite-sandbox/vite.config.ts b/test/emotion-vite-sandbox/vite.config.ts new file mode 100644 index 00000000000000..778324334d644b --- /dev/null +++ b/test/emotion-vite-sandbox/vite.config.ts @@ -0,0 +1,74 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { defineConfig, transformWithEsbuild } from 'vite'; +import react from '@vitejs/plugin-react'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); +// Two levels up: test/emotion-vite-sandbox → test → monorepo root +const MONOREPO_ROOT = path.resolve(dirname, '../..'); + +// https://vite.dev/config/ +// Use the function form so `mode` is available for the NODE_ENV define. +export default defineConfig(({ mode }) => ({ + // MUI source files reference process.env.NODE_ENV for dev-only warnings. + // Vite doesn't polyfill `process` in browser builds, so we define it explicitly. + define: { + 'process.env.NODE_ENV': JSON.stringify(mode === 'production' ? 'production' : 'development'), + }, + optimizeDeps: { + esbuildOptions: { + // MUI source files use .js extension but contain JSX syntax. + // Tell esbuild's pre-bundler to treat them as JSX. + loader: { '.js': 'jsx' }, + }, + }, + plugins: [ + // @mui/material source files use .js extension but contain JSX. + // This plugin re-parses them as JSX so Rollup/esbuild can handle them. + // enforce: 'pre' ensures this plugin runs before Vite's internal esbuild + // pre-transform, which would otherwise reject JSX in .js files. + { + name: 'treat-js-files-as-jsx', + enforce: 'pre' as const, + transform(code, id) { + if (/\/node_modules\//.test(id)) { + return null; + } + if (id.startsWith('\0')) { + return null; + } + if (!/.*\.js$/.test(id)) { + return null; + } + return transformWithEsbuild(code, id, { loader: 'jsx' }); + }, + }, + react(), + ], + resolve: { + alias: [ + // Resolve all @mui/* packages from monorepo source so we test the branch + // code, not a published release. + { + find: '@mui/material', + replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-material/src'), + }, + { + find: '@mui/system', + replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-system/src'), + }, + { + find: '@mui/utils', + replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-utils/src'), + }, + { + find: '@mui/private-theming', + replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-private-theming/src'), + }, + { + find: '@mui/styled-engine', + replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-styled-engine/src'), + }, + ], + }, +})); From 2fedbbfaa75b623885385e6e2b847c011a3209d3 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Tue, 16 Jun 2026 17:41:18 +0300 Subject: [PATCH 12/21] use useCSSColorScheme and useColorScheme to switch modes --- docs/pages/experiments/css-slider.tsx | 230 +++++++----------- docs/pages/experiments/emotion-slider.tsx | 227 +++++++---------- .../src/styles/CssThemeProvider.tsx | 157 ++++++++++-- packages/mui-material/src/styles/index.d.ts | 8 +- packages/mui-material/src/styles/index.js | 6 +- .../src/cssVars/createCssVarsProvider.js | 112 ++------- .../src/cssVars/useColorSchemeSetup.ts | 200 +++++++++++++++ .../css-theme-provider-vite-sandbox/README.md | 16 +- .../src/App.tsx | 126 +++++----- test/emotion-vite-sandbox/src/App.tsx | 112 ++++----- 10 files changed, 665 insertions(+), 529 deletions(-) create mode 100644 packages/mui-system/src/cssVars/useColorSchemeSetup.ts diff --git a/docs/pages/experiments/css-slider.tsx b/docs/pages/experiments/css-slider.tsx index ae3595a3ce3c9d..18514d1a3d9c40 100644 --- a/docs/pages/experiments/css-slider.tsx +++ b/docs/pages/experiments/css-slider.tsx @@ -1,13 +1,7 @@ 'use client'; import * as React from 'react'; -import { createTheme, CssThemeProvider, styled } from '@mui/material/styles'; -import Box from '@mui/material/Box'; +import { createTheme, CssThemeProvider, styled, useCssColorScheme } from '@mui/material/styles'; import Slider from '@mui/material/Slider'; -import Stack from '@mui/material/Stack'; -import Switch from '@mui/material/Switch'; -import ToggleButton from '@mui/material/ToggleButton'; -import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; -import Typography from '@mui/material/Typography'; // ---------- Themes ---------------------------------------------------------- @@ -68,26 +62,6 @@ const GradientSlider = styled(Slider)({ }, }); -function Section({ label, children }: { label: string; children: React.ReactNode }) { - return ( - - - {label} - - {children} - - ); -} - // ---------- useMounted guard (avoid SSR hydration mismatch) ---------------- function useMounted() { @@ -105,140 +79,106 @@ function Page({ themeKey: ThemeKey; setThemeKey: (key: ThemeKey) => void; }) { - const [mode, setMode] = React.useState<'light' | 'dark'>('light'); + const { mode, setMode } = useCssColorScheme(); const mounted = useMounted(); - const [value1, setValue1] = React.useState(40); - const [value2, setValue2] = React.useState(60); + const [value1, setValue1] = React.useState(60); + const [value2, setValue2] = React.useState(40); const [value3, setValue3] = React.useState(30); - const [value4, setValue4] = React.useState([20, 70]); return ( - - {/* Header */} - - CssThemeProvider + Slider - - - Verifies Slider still works with the CSS-only path: sx,{' '} - styled(), styleOverrides, and defaultProps via{' '} - createTheme. - - - {/* ── Theme + dark mode switcher ──────────────────────────────────── */} -
- - v && setThemeKey(v as ThemeKey)} - size="small" +

CssThemeProvider + Slider

+

+ Verifies Slider works with the CSS-only path. Engine: @mui/styled-engine-noop — + no Emotion. +

+ + {/* Theme switcher */} +
+ {(Object.keys(themes) as ThemeKey[]).map((key) => ( +
- - {/* ── Default ─────────────────────────────────────────────────────── */} -
- setValue1(v as number)} /> - - Value: {value1} - -
- - {/* ── sx prop ─────────────────────────────────────────────────────── */} -
+ {key} + + ))} + + + {/* Dark mode toggle */} + + + {/* ── sx prop ──────────────────────────────────────────────────── */} +

sx prop — secondary color + custom thumb:

+
setValue2(v as number)} + value={value1} + onChange={(_, v) => setValue1(v as number)} sx={{ color: 'var(--mui-palette-secondary-main)', - '& .MuiSlider-thumb': { - width: 22, - height: 22, - boxShadow: '0 0 0 7px color-mix(in srgb, var(--mui-palette-secondary-main) 16%, transparent)', - }, - '& .MuiSlider-rail': { opacity: 0.3 }, + '& .MuiSlider-thumb': { width: 22, height: 22 }, }} + aria-label="sx slider" /> - - Value: {value2} - -
- - {/* ── styled() ────────────────────────────────────────────────────── */} -
- setValue3(v as number)} /> - - Value: {value3} - -
- - {/* ── Range slider ────────────────────────────────────────────────── */} -
+ +

Value: {value1}

+ + {/* ── className + style tag ─────────────────────────────────────── */} +

+ className override — thumb should be purple via plain CSS: +

+
setValue4(v as number[])} - marks - step={10} - valueLabelDisplay="auto" - sx={{ '& .MuiSlider-markLabel': { fontSize: '0.7rem' } }} + value={value2} + onChange={(_, v) => setValue2(v as number)} + className="css-custom-slider" + aria-label="className slider" + /> +
+

Value: {value2}

+ + {/* ── styled() ────────────────────────────────────────────────── */} +

styled() — gradient track:

+
+ setValue3(v as number)} + aria-label="gradient slider" /> - - Value: [{value4.join(', ')}] - -
- - {/* ── Disabled ────────────────────────────────────────────────────── */} -
- -
- - {/* ── Vertical ────────────────────────────────────────────────────── */} -
- - setValue1(v as number)} - /> - setValue2(v as number)} - sx={{ color: 'var(--mui-palette-secondary-main)' }} - /> - -
-
+ +

Value: {value3}

+ + + ); } diff --git a/docs/pages/experiments/emotion-slider.tsx b/docs/pages/experiments/emotion-slider.tsx index 13baee2ea60910..37f0fc2efe846c 100644 --- a/docs/pages/experiments/emotion-slider.tsx +++ b/docs/pages/experiments/emotion-slider.tsx @@ -1,13 +1,7 @@ 'use client'; import * as React from 'react'; import { createTheme, ThemeProvider, styled, useColorScheme } from '@mui/material/styles'; -import Box from '@mui/material/Box'; import Slider from '@mui/material/Slider'; -import Stack from '@mui/material/Stack'; -import Switch from '@mui/material/Switch'; -import ToggleButton from '@mui/material/ToggleButton'; -import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; -import Typography from '@mui/material/Typography'; // ---------- Themes ---------------------------------------------------------- @@ -66,28 +60,6 @@ const GradientSlider = styled(Slider)(({ theme }) => ({ }, })); -// ---------- Layout helpers -------------------------------------------------- - -function Section({ label, children }: { label: string; children: React.ReactNode }) { - return ( - - - {label} - - {children} - - ); -} - // ---------- useMounted guard (avoid SSR hydration mismatch) ---------------- function useMounted() { @@ -107,137 +79,104 @@ function Page({ }) { const { mode, setMode } = useColorScheme(); const mounted = useMounted(); - const [value1, setValue1] = React.useState(40); - const [value2, setValue2] = React.useState(60); + const [value1, setValue1] = React.useState(60); + const [value2, setValue2] = React.useState(40); const [value3, setValue3] = React.useState(30); - const [value4, setValue4] = React.useState([20, 70]); return ( - - {/* Header */} - - Emotion + Slider - - - Verifies Slider still works with Emotion customization: sx,{' '} - styled(), styleOverrides, and defaultProps via{' '} - createTheme. - - - {/* ── Theme + dark mode switcher ──────────────────────────────────── */} -
- - v && setThemeKey(v as ThemeKey)} - size="small" +

Emotion + Slider

+

+ Verifies Slider works with the Emotion path. Engine: @mui/styled-engine{' '} + (Emotion). +

+ + {/* Theme switcher */} +
+ {(Object.keys(themes) as ThemeKey[]).map((key) => ( +
- - {/* ── Default ─────────────────────────────────────────────────────── */} -
- setValue1(v as number)} /> - - Value: {value1} - -
- - {/* ── sx prop ─────────────────────────────────────────────────────── */} -
+ {key} + + ))} + + + {/* Dark mode toggle */} + + + {/* ── sx prop ──────────────────────────────────────────────────── */} +

sx prop — secondary color + custom thumb:

+
setValue2(v as number)} + value={value1} + onChange={(_, v) => setValue1(v as number)} sx={{ color: 'secondary.main', - '& .MuiSlider-thumb': { - width: 22, - height: 22, - boxShadow: (t) => `0 0 0 7px ${t.palette.secondary.main}28`, - }, - '& .MuiSlider-rail': { opacity: 0.3 }, + '& .MuiSlider-thumb': { width: 22, height: 22 }, }} + aria-label="sx slider" /> - - Value: {value2} - -
- - {/* ── styled() ────────────────────────────────────────────────────── */} -
- setValue3(v as number)} /> - - Value: {value3} - -
- - {/* ── Range slider ────────────────────────────────────────────────── */} -
+ +

Value: {value1}

+ + {/* ── className + style tag ─────────────────────────────────────── */} +

+ className override — thumb should be purple via plain CSS: +

+
setValue4(v as number[])} - marks - step={10} - valueLabelDisplay="auto" - sx={{ '& .MuiSlider-markLabel': { fontSize: '0.7rem' } }} + value={value2} + onChange={(_, v) => setValue2(v as number)} + className="emotion-custom-slider" + aria-label="className slider" + /> +
+

Value: {value2}

+ + {/* ── styled() ────────────────────────────────────────────────── */} +

styled() — gradient track:

+
+ setValue3(v as number)} + aria-label="gradient slider" /> - - Value: [{value4.join(', ')}] - -
- - {/* ── Disabled ────────────────────────────────────────────────────── */} -
- -
- - {/* ── Vertical ────────────────────────────────────────────────────── */} -
- - setValue1(v as number)} - /> - setValue2(v as number)} - sx={{ color: 'secondary.main' }} - /> - -
-
+ +

Value: {value3}

+ + + ); } diff --git a/packages/mui-material/src/styles/CssThemeProvider.tsx b/packages/mui-material/src/styles/CssThemeProvider.tsx index 9e5b6457b87d8d..aa5a1f7adbbb5e 100644 --- a/packages/mui-material/src/styles/CssThemeProvider.tsx +++ b/packages/mui-material/src/styles/CssThemeProvider.tsx @@ -3,8 +3,44 @@ import * as React from 'react'; import type { DefaultTheme } from '@mui/system'; import RtlProvider from '@mui/system/RtlProvider'; import DefaultPropsProvider from '@mui/system/DefaultPropsProvider'; +import useColorSchemeSetup from '@mui/system/cssVars/useColorSchemeSetup'; import CssVarsInjector from './CssVarsInjector'; import THEME_ID from './identifier'; +import { defaultConfig } from '../InitColorSchemeScript/InitColorSchemeScript'; + +export interface CssColorSchemeContextValue { + allColorSchemes: string[]; + colorScheme: string | undefined; + mode: 'light' | 'dark' | 'system' | undefined; + systemMode: 'light' | 'dark' | undefined; + lightColorScheme: string; + darkColorScheme: string; + setMode: (mode: 'light' | 'dark' | 'system' | null) => void; + setColorScheme: ( + colorScheme: string | null | Partial<{ light: string | null; dark: string | null }>, + ) => void; +} + +export const CssColorSchemeContext = React.createContext( + undefined, +); + +if (process.env.NODE_ENV !== 'production') { + CssColorSchemeContext.displayName = 'CssColorSchemeContext'; +} + +export function useCssColorScheme(): CssColorSchemeContextValue { + const ctx = React.useContext(CssColorSchemeContext); + if (!ctx) { + throw /* minify-error */ new Error( + 'MUI: `useCssColorScheme` must be called inside a `CssThemeProvider`. ' + + 'See https://mui.com/r/css-theme-provider for more info.', + ); + } + return ctx; +} + +// --------------------------------------------------------------------------- export interface CssThemeProviderProps { children?: React.ReactNode; @@ -27,6 +63,39 @@ export interface CssThemeProviderProps { * A nonce attribute to add to the injected ` + + ); +} - - +export default function App() { + return ( + + ); } diff --git a/test/emotion-vite-sandbox/src/App.tsx b/test/emotion-vite-sandbox/src/App.tsx index 7b7082dd31f5e5..dd7a1478303e80 100644 --- a/test/emotion-vite-sandbox/src/App.tsx +++ b/test/emotion-vite-sandbox/src/App.tsx @@ -19,7 +19,7 @@ * # should print matches */ import * as React from 'react'; -import { createTheme, ThemeProvider } from '@mui/material/styles'; +import { createTheme, ThemeProvider, useColorScheme } from '@mui/material/styles'; import Slider from '@mui/material/Slider'; const theme = createTheme({ @@ -29,71 +29,71 @@ const theme = createTheme({ colorSchemes: { light: true, dark: true }, }); -type Mode = 'light' | 'dark'; - -export default function App() { +function AppContent() { const [value, setValue] = React.useState(40); - const [mode, setMode] = React.useState('light'); - - function toggleMode() { - const next: Mode = mode === 'light' ? 'dark' : 'light'; - document.documentElement.setAttribute('data-mui-color-scheme', next); - setMode(next); - } + const { mode, setMode } = useColorScheme(); return ( - -
+

emotion-engine sandbox

+

+ Engine: @mui/styled-engine (Emotion) — Emotion is bundled here. +

+ + - {/* Dark mode toggle */} - +
+

Slider value: {value}

-
-

Slider value: {value}

+ {/* sx prop — works with Emotion, applies color override at runtime */} + setValue(v as number)} + sx={{ color: 'secondary.main' }} + aria-label="Sandbox slider" + /> - {/* sx prop — works with Emotion, applies color override at runtime */} - setValue(v as number)} - sx={{ color: 'secondary.main' }} - aria-label="Sandbox slider" - /> +

+ className override (thumb should be purple, overriding the default via plain CSS): +

-

- className override (thumb should be purple, overriding the default via plain CSS): -

+ setValue(v as number)} + className="custom-slider" + aria-label="Custom class slider" + /> +
- {/* className override */} - setValue(v as number)} - className="custom-slider" - aria-label="Custom class slider" - /> -
+ +
+ ); +} - - +export default function App() { + return ( + + ); } From b8e9d6d61f1260f619c7791f2d82b168c184ba81 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Thu, 18 Jun 2026 17:32:38 +0300 Subject: [PATCH 13/21] export useColorSchemeSetup from mui-system --- packages/mui-system/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/mui-system/package.json b/packages/mui-system/package.json index a36519cd5098aa..eba1c3d08c1a96 100644 --- a/packages/mui-system/package.json +++ b/packages/mui-system/package.json @@ -88,6 +88,7 @@ "./createBreakpoints": "./src/createBreakpoints/index.js", "./createTheme": "./src/createTheme/index.js", "./RtlProvider": "./src/RtlProvider/index.js", + "./cssVars/useColorSchemeSetup": "./src/cssVars/useColorSchemeSetup.ts", "./styleFunctionSx": "./src/styleFunctionSx/index.js", "./ThemeProvider": "./src/ThemeProvider/index.js", "./useThemeProps": "./src/useThemeProps/index.js", From bc2239a52feeb5e7e82e200460b22075eec6ad32 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Thu, 18 Jun 2026 17:32:51 +0300 Subject: [PATCH 14/21] include css files --- packages/mui-material/package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/mui-material/package.json b/packages/mui-material/package.json index 3692d62f304084..152c352fce21fd 100644 --- a/packages/mui-material/package.json +++ b/packages/mui-material/package.json @@ -25,7 +25,7 @@ "url": "https://opencollective.com/mui-org" }, "scripts": { - "build": "code-infra build --tsgo --flat", + "build": "code-infra build --tsgo --flat --copy \"src/**/*.css\"", "release": "pnpm build && pnpm publish", "test": "pnpm --workspace-root test:unit --project \"*:@mui/material\"", "typescript": "tsgo -p tsconfig.json", @@ -83,7 +83,9 @@ "optional": true } }, - "sideEffects": false, + "sideEffects": [ + "*.css" + ], "publishConfig": { "access": "public", "directory": "build" From d9020d3fe7af50e928ea3d354b7c578d58732f68 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Thu, 18 Jun 2026 17:33:04 +0300 Subject: [PATCH 15/21] make vite apps use build instead of source --- .../vite.config.ts | 33 ++++++------- test/emotion-vite-sandbox/vite.config.ts | 46 ++++++++----------- 2 files changed, 34 insertions(+), 45 deletions(-) diff --git a/test/css-theme-provider-vite-sandbox/vite.config.ts b/test/css-theme-provider-vite-sandbox/vite.config.ts index 10052c6ef5282e..da7b6ef183e923 100644 --- a/test/css-theme-provider-vite-sandbox/vite.config.ts +++ b/test/css-theme-provider-vite-sandbox/vite.config.ts @@ -47,24 +47,21 @@ export default defineConfig(({ mode }) => ({ ], resolve: { alias: [ - // Resolve all @mui/* packages from monorepo source so we test the branch - // code, not a published release. - { - find: '@mui/material', - replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-material/src'), - }, - { - find: '@mui/system', - replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-system/src'), - }, - { - find: '@mui/utils', - replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-utils/src'), - }, - { - find: '@mui/private-theming', - replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-private-theming/src'), - }, + // These aliases used to point @mui/* packages at their monorepo `src/` + // directories so Vite would pick up in-flight source changes without a + // rebuild. Now that the packages are built (pnpm -F @mui/material build + // etc.), the workspace symlinks + each package's `exports` field in + // build/package.json are enough — no alias needed. + // + // If you want to test against source again (e.g. mid-refactor before + // rebuilding), un-comment these and re-add the `treat-js-files-as-jsx` + // plugin (source .js files contain JSX; built .mjs files do not). + // + // { find: '@mui/material', replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-material/src') }, + // { find: '@mui/system', replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-system/src') }, + // { find: '@mui/utils', replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-utils/src') }, + // { find: '@mui/private-theming', replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-private-theming/src') }, + // THE KEY ALIAS: swap Emotion for the zero-runtime noop engine. // This is exactly what non-Emotion users configure in their own bundler. { diff --git a/test/emotion-vite-sandbox/vite.config.ts b/test/emotion-vite-sandbox/vite.config.ts index 778324334d644b..2cacd2a92a3af8 100644 --- a/test/emotion-vite-sandbox/vite.config.ts +++ b/test/emotion-vite-sandbox/vite.config.ts @@ -1,11 +1,10 @@ -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; +// import path from 'node:path'; +// import { fileURLToPath } from 'node:url'; import { defineConfig, transformWithEsbuild } from 'vite'; import react from '@vitejs/plugin-react'; -const dirname = path.dirname(fileURLToPath(import.meta.url)); -// Two levels up: test/emotion-vite-sandbox → test → monorepo root -const MONOREPO_ROOT = path.resolve(dirname, '../..'); +// const dirname = path.dirname(fileURLToPath(import.meta.url)); +// const MONOREPO_ROOT = path.resolve(dirname, '../..'); // https://vite.dev/config/ // Use the function form so `mode` is available for the NODE_ENV define. @@ -47,28 +46,21 @@ export default defineConfig(({ mode }) => ({ ], resolve: { alias: [ - // Resolve all @mui/* packages from monorepo source so we test the branch - // code, not a published release. - { - find: '@mui/material', - replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-material/src'), - }, - { - find: '@mui/system', - replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-system/src'), - }, - { - find: '@mui/utils', - replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-utils/src'), - }, - { - find: '@mui/private-theming', - replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-private-theming/src'), - }, - { - find: '@mui/styled-engine', - replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-styled-engine/src'), - }, + // These aliases used to point @mui/* packages at their monorepo `src/` + // directories so Vite would pick up in-flight source changes without a + // rebuild. Now that the packages are built (pnpm -F @mui/material build + // etc.), the workspace symlinks + each package's `exports` field in + // build/package.json are enough — no alias needed. + // + // If you want to test against source again (e.g. mid-refactor before + // rebuilding), un-comment these and re-add the `treat-js-files-as-jsx` + // plugin (source .js files contain JSX; built .mjs files do not). + // + // { find: '@mui/material', replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-material/src') }, + // { find: '@mui/system', replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-system/src') }, + // { find: '@mui/utils', replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-utils/src') }, + // { find: '@mui/private-theming', replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-private-theming/src') }, + // { find: '@mui/styled-engine', replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-styled-engine/src') }, ], }, })); From f1f4ec21000a90338ac51c992693e5165f442bc6 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Thu, 18 Jun 2026 17:38:27 +0300 Subject: [PATCH 16/21] fix readme --- test/css-theme-provider-vite-sandbox/README.md | 6 +++--- test/emotion-vite-sandbox/README.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/css-theme-provider-vite-sandbox/README.md b/test/css-theme-provider-vite-sandbox/README.md index c601665eda0e3a..cffe22f14e916d 100644 --- a/test/css-theme-provider-vite-sandbox/README.md +++ b/test/css-theme-provider-vite-sandbox/README.md @@ -1,7 +1,7 @@ # css-theme-provider-vite-sandbox A minimal Vite + React app that exercises **Path A (Mantine analogy)** for -Material UI components — noop engine + runtime `CssThemeProvider`. +Material UI components — noop engine + runtime `CssThemeProvider`. ## Purpose @@ -17,7 +17,7 @@ Verifies that: 5. `className`-based overrides beat `@layer mui.default` without `!important`. 6. Dark mode works via `CssThemeProvider` by flipping `data-mui-color-scheme` on `document.documentElement`. -7. `useTheme()` returns live JS theme values (breakpoints, spacing, etc.). +7. `useTheme()` returns live JavaScript theme values (breakpoints, spacing, etc.). ## How this differs from the other sandboxes @@ -56,7 +56,7 @@ grep -r "@emotion/react\|@emotion/styled\|@emotion/cache\|EmotionCacheContext\|i ## Bundle size comparison -Build all three sandboxes, then compare JS output sizes to measure the overhead +Build all three sandboxes, then compare JavaScript output sizes to measure the overhead of each approach: ```bash diff --git a/test/emotion-vite-sandbox/README.md b/test/emotion-vite-sandbox/README.md index 21aa48fe3ce1c6..b870c1c17ea8f6 100644 --- a/test/emotion-vite-sandbox/README.md +++ b/test/emotion-vite-sandbox/README.md @@ -1,7 +1,7 @@ # emotion-vite-sandbox A minimal Vite + React app that exercises the **Emotion (default) path** for -Material UI components. +Material UI components. ## Purpose @@ -48,7 +48,7 @@ grep -r "@emotion" test/emotion-vite-sandbox/dist/ ## Bundle size comparison -Build all three sandboxes, then compare JS output sizes to measure the overhead +Build all three sandboxes, then compare JavaScript output sizes to measure the overhead of each approach: ```bash @@ -71,4 +71,4 @@ ls -lh test/emotion-vite-sandbox/dist/assets/*.css This sandbox maps to **§8 (Bundle size validation)** in `STYLING_V8_TODO.md`. It is the Emotion-path counterpart to `test/noop-vite-sandbox`, used to -measure the JS bundle delta between the two engines. +measure the JavaScript bundle delta between the two engines. From d561b4a06fae8e3765b1b16db12e977a62dbc563 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Thu, 18 Jun 2026 17:39:06 +0300 Subject: [PATCH 17/21] pnpm dedupe --- pnpm-lock.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3c774e3bf6525..0237339c478dde 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1595,7 +1595,7 @@ importers: version: link:../packages/mui-utils/build '@tailwindcss/vite': specifier: ^4.2.4 - version: 4.3.0(vite@8.0.16(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.3.0(vite@7.3.5(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) '@testing-library/dom': specifier: 10.4.1 version: 10.4.1 @@ -13829,7 +13829,7 @@ snapshots: eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)) eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)) eslint-plugin-compat: 7.0.1(eslint@10.3.0(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@10.3.0(jiti@2.6.1)) eslint-plugin-mdx: 3.7.0(eslint@10.3.0(jiti@2.6.1)) eslint-plugin-mocha: 11.2.0(eslint@10.3.0(jiti@2.6.1)) @@ -15523,12 +15523,12 @@ snapshots: postcss: 8.5.15 tailwindcss: 4.2.4 - '@tailwindcss/vite@4.3.0(vite@8.0.16(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0))': + '@tailwindcss/vite@4.3.0(vite@7.3.5(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.0 '@tailwindcss/oxide': 4.3.0 tailwindcss: 4.3.0 - vite: 8.0.16(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 7.3.5(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0) '@tanstack/query-core@5.100.9': {} @@ -17650,7 +17650,7 @@ snapshots: tinyglobby: 0.2.17 unrs-resolver: 1.9.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -17700,7 +17700,7 @@ snapshots: lodash: 4.18.1 pkg-dir: 5.0.0 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 From f739cd076c945eb6aacbca10a324c4d576057e2b Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Fri, 19 Jun 2026 10:26:37 +0300 Subject: [PATCH 18/21] update pnpm-lock --- pnpm-lock.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0237339c478dde..9428537c1cdd7e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13826,10 +13826,10 @@ snapshots: es-toolkit: 1.46.1 eslint: 10.3.0(jiti@2.6.1) eslint-config-prettier: 10.1.8(eslint@10.3.0(jiti@2.6.1)) - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)) - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)))(eslint@10.3.0(jiti@2.6.1)) - eslint-plugin-compat: 7.0.1(eslint@10.3.0(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.6.1)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@10.3.0(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.6.1)) + eslint-plugin-compat: 7.0.2(eslint@10.3.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@10.3.0(jiti@2.6.1)) eslint-plugin-mdx: 3.7.0(eslint@10.3.0(jiti@2.6.1)) eslint-plugin-mocha: 11.2.0(eslint@10.3.0(jiti@2.6.1)) @@ -17650,7 +17650,7 @@ snapshots: tinyglobby: 0.2.17 unrs-resolver: 1.9.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -17700,7 +17700,7 @@ snapshots: lodash: 4.18.1 pkg-dir: 5.0.0 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 From 09453a6e4c5c804d0776d290022182867e677366 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Fri, 19 Jun 2026 12:39:21 +0300 Subject: [PATCH 19/21] alias with built styled-engine-noop --- test/css-theme-provider-vite-sandbox/vite.config.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/css-theme-provider-vite-sandbox/vite.config.ts b/test/css-theme-provider-vite-sandbox/vite.config.ts index da7b6ef183e923..adf97006a054e9 100644 --- a/test/css-theme-provider-vite-sandbox/vite.config.ts +++ b/test/css-theme-provider-vite-sandbox/vite.config.ts @@ -1,11 +1,11 @@ -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; +// import path from 'node:path'; +// import { fileURLToPath } from 'node:url'; import { defineConfig, transformWithEsbuild } from 'vite'; import react from '@vitejs/plugin-react'; -const dirname = path.dirname(fileURLToPath(import.meta.url)); +// const dirname = path.dirname(fileURLToPath(import.meta.url)); // Two levels up: test/css-theme-provider-vite-sandbox → test → monorepo root -const MONOREPO_ROOT = path.resolve(dirname, '../..'); +// const MONOREPO_ROOT = path.resolve(dirname, '../..'); // https://vite.dev/config/ // Use the function form so `mode` is available for the NODE_ENV define. @@ -66,7 +66,7 @@ export default defineConfig(({ mode }) => ({ // 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'), + replacement: '@mui/styled-engine-noop', }, ], }, From 3d498bb66dac35c76140728ed334feef17e9005d Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Fri, 19 Jun 2026 12:43:26 +0300 Subject: [PATCH 20/21] docs ignore cssvarsinjector --- packages/mui-material/src/styles/CssVarsInjector.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/mui-material/src/styles/CssVarsInjector.tsx b/packages/mui-material/src/styles/CssVarsInjector.tsx index 3b19bb819ef7ca..9374d2e8ce2e0c 100644 --- a/packages/mui-material/src/styles/CssVarsInjector.tsx +++ b/packages/mui-material/src/styles/CssVarsInjector.tsx @@ -1,3 +1,6 @@ +/** + * @ignore - internal component. + */ import { styleSheetsToString } from '@mui/system'; import * as React from 'react'; From 7dcfbad1c0f787e044bbd36b0cb6c21e8a50a0ab Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Fri, 19 Jun 2026 14:11:00 +0300 Subject: [PATCH 21/21] extract-error-codes --- docs/public/static/error-codes.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/public/static/error-codes.json b/docs/public/static/error-codes.json index 1c94b8d8935583..178303977c3c58 100644 --- a/docs/public/static/error-codes.json +++ b/docs/public/static/error-codes.json @@ -18,7 +18,9 @@ "17": "MUI: Expected valid input target. Did you use a custom `slots.input` and forget to forward refs? See https://mui.com/r/input-component-ref-interface for more info.", "18": "MUI: The provided shorthand %s is invalid. The format should be `@` or `@/`.\nFor example, `@sm` or `@600` or `@40rem/sidebar`.", "19": "MUI: The `experimental_sx` has been moved to `theme.unstable_sx`.For more details, see https://github.com/mui/material-ui/pull/35150.", - "20": "MUI: `vars` is a private field used for CSS variables support.\nPlease use another name or follow the [docs](https://mui.com/material-ui/customization/css-theme-variables/usage/) to enable the feature.", "21": "MUI: The `colorSchemes.%s` option is either missing or invalid.", - "22": "MUI: `vars` is a private field used for CSS variables support.\nPlease use another name or follow the [docs](https://mui.com/material-ui/customization/css-theme-variables/usage/) to enable the feature." + "22": "MUI: `vars` is a private field used for CSS variables support.\nPlease use another name or follow the [docs](https://mui.com/material-ui/customization/css-theme-variables/usage/) to enable the feature.", + "23": "MUI: `useCssColorScheme` must be called inside a `CssThemeProvider`. See https://mui.com/r/css-theme-provider for more info.", + "24": "MUI: CssThemeProvider does not accept a theme function. Pass a theme object created with `createTheme({ cssVariables: true })` instead. See https://mui.com/r/css-theme-provider for more info.", + "25": "MUI: CssThemeProvider requires a theme created with `cssVariables: true`. Without it, the imported component CSS files reference variables that are never generated, leaving components unstyled. See https://mui.com/r/css-theme-provider for more info." }