diff --git a/CHANGELOG.old.md b/CHANGELOG.old.md
index cc0beb5bf4942d..cf53c1039f0f0e 100644
--- a/CHANGELOG.old.md
+++ b/CHANGELOG.old.md
@@ -98,7 +98,7 @@ A big thanks to the 16 contributors who made this release possible.
- Fix incorrect indentation in migration guide (#47571) @sai6855
- Enable MUIΒ chat on MaterialΒ UI demos (#46837) @siriwatknp
- Add docs and website banner for Dev survey'25 (#47521) @prakhargupta1
-- Update Tailwind CSS v4 + Next.js Pages Router docs (#47546) @atharva3333
+- Update TailwindΒ CSS v4 + Next.js Pages Router docs (#47546) @atharva3333
- Add warning callout to Sync plugin doc (#47511) @mapache-salvaje
- Update typo in TailwindCSS v4 integration with Next.js docs (#47512) @TimKraemer
- Fix link to contributing guide (#47473) @oliviertassinari
@@ -3738,7 +3738,7 @@ This release was mostly about π bug fixes and π documentation improvements
- Link to pnpm installation docs (#42420) @aarongarciah
- Remove LocalMonero (@oliviertassinari) (#42315) @github-actions[bot]
-- [material-ui] Fix typo in style interoperability with Tailwind CSS docs (@ZeeshanTamboli) (#42312) @github-actions[bot]
+- [material-ui] Fix typo in style interoperability with TailwindΒ CSS docs (@ZeeshanTamboli) (#42312) @github-actions[bot]
- [material-ui][Pagination] Clarify pagination `page` prop API (@Mandar-Pandya) (#42265) @github-actions[bot]
- [material-ui][Tabs] Improve the Basic Tabs demo (@MatheusEli) (#42426) @github-actions[bot]
- [pigment-css] Fix duplication of content (#42410) @oliviertassinari
diff --git a/STYLING_V8_TODO.md b/STYLING_V8_TODO.md
new file mode 100644
index 00000000000000..70e6b3f01b0a4a
--- /dev/null
+++ b/STYLING_V8_TODO.md
@@ -0,0 +1,297 @@
+# Styling v8 β Emotion-free path TODO
+
+## Goal
+
+Material UI components ship their **own base styles as plain CSS files** (`@layer mui.default`,
+driven by `--mui-*` CSS variables). This base layer is shared by all consumers. Users who want
+Emotion (`sx`, `styled()`) keep using it without any changes. Users who don't want Emotion
+swap the engine and use one of two theme delivery paths.
+
+---
+
+## Path overview
+
+| | **Emotion path** | **Path A β CssThemeProvider** | **Path B β Static CSS** |
+|---|---|---|---|
+| Engine | `@mui/styled-engine` (Emotion) | `@mui/styled-engine-noop` | `@mui/styled-engine-noop` |
+| Base styles | `Component.css` | same | same |
+| Theme delivery | `ThemeProvider` (runtime) | `CssThemeProvider` (runtime) | `default-theme.css` (build-time) |
+| Custom theme | `createTheme()` β `ThemeProvider` | `createTheme({ cssVariables: true })` β `CssThemeProvider` | `generateThemeCss(createTheme(...))` β import `.css` |
+| `useColorScheme()` | β via `ThemeProvider` | β via `CssThemeProvider` | β standalone hook (no provider) |
+| SSR flash prevention | β `InitColorSchemeScript` | β `InitColorSchemeScript` | β `InitColorSchemeScript` |
+| `useTheme()` (JS values) | β | β | β no theme context |
+| `sx` prop | β | β ignored + dev warning | β ignored + dev warning |
+| `styled()` overrides | β | β | β |
+| Analogy | β | Mantine | Radix Themes |
+
+**Breaking changes policy:** The Emotion path has zero breaking changes. Paths A and B are
+opt-in via the engine alias. `sx` is intentionally unsupported on both non-Emotion paths.
+
+---
+
+## Shared prerequisite β Component CSS conversion _(blocks both non-Emotion paths)_
+
+Both non-Emotion paths require components to have their styles extracted to co-located `.css`
+files. Without this, components have no base styles on the noop engine. This is the largest
+body of work and is independent of the provider/static choice.
+
+### The pattern (established by Slider)
+
+- `Slider.js` β `styled()` calls with empty payloads: `styled('span', { name, slot })({})`.
+ The engine alias determines at bundle time whether this is Emotion or noop.
+- `Slider.css` β all base styles inside `@layer mui.default`, using `--mui-*` CSS variables.
+ Auto-imported via a side-effect import at the top of `Slider.js`. Users never import it manually.
+- Both Emotion and noop users get the same visual output from the same component file.
+
+### CSS file convention
+
+- `@layer mui.default` wrapper for all rules.
+- Class naming: `.MuiX-slotName`, `.MuiX-variantName`, `.Mui-stateClass`.
+- Logical properties (`inset-block-start`, `inset-inline-start`) instead of `top`/`left`/`right`/`bottom`.
+- Media queries: `forced-colors`, `pointer: coarse`, `prefers-reduced-motion`.
+- Print: `print-color-adjust: exact` (not the deprecated `color-adjust`).
+
+### TODOs β component conversion
+
+- [ ] Verify `Slider.js` is a full drop-in for `SliderEmotion.js` under the Emotion engine.
+- [ ] Remove `SliderEmotion.js`, `SliderEmotion.d.ts`, and the `SliderEmotion` re-exports once confirmed.
+- [ ] Convert `Button` as the second pilot (variants Γ colors Γ states, ripple, focus-visible).
+- [ ] Capture a repeatable conversion checklist from the Slider + Button work.
+- [ ] Inventory remaining components and sequence the rollout.
+
+---
+
+## Shared prerequisite β Engine alias _(blocks both non-Emotion paths)_
+
+Aliasing `@mui/styled-engine` to `@mui/styled-engine-noop` is the single opt-in step for both
+non-Emotion paths. The noop engine exists and works. The gap is documentation.
+
+### TODOs β engine alias
+
+- [ ] Document per-bundler alias config (Vite, webpack, Next.js, esbuild).
+- [ ] Decide whether to also offer Option B or Option C for zero-config setup, or defer:
+ - **Option A (chosen approach, documented in `test/noop-vite-sandbox/vite.config.ts`)** β
+ bundler alias: user adds one alias rule to their bundler config (`@mui/styled-engine` β
+ `@mui/styled-engine-noop`). Works with any bundler, no import changes required, but requires
+ touching build config. End-user documentation is still a TODO above.
+ - **Option B β subpath imports** (e.g. `@mui/material/css/Slider`): a separate set of package
+ subpaths that resolve directly to the noop variant without any bundler config. Zero setup, but
+ users must change every component import in their app (`/Slider` β `/css/Slider`), making
+ migration tedious and mixing Emotion and noop imports in the same tree possible by accident.
+ - **Option C β `exports` condition**: `package.json` `exports` supports custom conditions (e.g.
+ `"mui-noop"`). Users activate it with a single bundler option (e.g. `resolve.conditions:
+ ['mui-noop']` in Vite), and all `@mui/material` imports automatically resolve to the noop
+ variant β no alias, no import changes. Cleanest DX, but custom conditions have uneven bundler
+ support and add permanent complexity to the package's `exports` map.
+- [ ] Add a CI test: build the non-Emotion entry point, assert **zero** `@emotion/*` modules.
+- [ ] Improve the noop engine dev warning: link to the migration guide, fire once per
+ component type rather than per instance.
+
+### Done β engine alias
+
+- β `@mui/styled-engine-noop` β zero-runtime passthrough, warns on `sx`, stubs `keyframes`/`css`.
+- β Confirmed: production build of `test/noop-vite-sandbox` contains zero `@emotion/*` runtime.
+ (The `__emotion_real` string in the output is a property-name guard in `@mui/system/createStyled`,
+ not an import.)
+- β Two gotchas documented for Vite: `process.env.NODE_ENV` must be defined via `vite.config.ts`
+ `define`; a JSX-in-`.js` transform plugin is required because MUI source uses `.js` + JSX.
+
+---
+
+## Path A β CssThemeProvider _(non-Emotion, runtime)_
+
+The Mantine analogy. Users write `createTheme()` in JS, pass it to a provider. The provider
+renders CSS variables into the document at runtime β no Emotion, no build step. API-identical
+to `ThemeProvider` minus `sx` and `styled()`.
+
+### What users need to do (Path A)
+
+```tsx
+// 1. Alias engine in bundler config (see Β§Engine alias above)
+
+// 2. Create a theme (must use cssVariables: true)
+import { createTheme } from '@mui/material/styles';
+const theme = createTheme({ cssVariables: true, colorSchemes: { light: true, dark: true } });
+
+// 3. Wrap the app β no ThemeProvider, no Emotion
+import { CssThemeProvider } from '@mui/material/styles';
+
+
+
+
+// 4. Dark mode toggle
+import { useColorScheme } from '@mui/material/styles'; // from CssThemeProvider context
+const { mode, setMode } = useColorScheme();
+setMode('dark');
+
+// 5. JS theme values (breakpoints, transitions, spacing)
+import { useTheme } from '@mui/material/styles';
+const theme = useTheme(); // theme.breakpoints.up('md'), etc.
+```
+
+SSR flash prevention works the same as with `ThemeProvider`: place ``
+in `
` before React hydrates. No extra steps specific to `CssThemeProvider` β once
+`useCurrentColorScheme` is wired inside it with the same default storage keys and attribute,
+`InitColorSchemeScript` is automatically compatible.
+
+### TODOs β Path A
+
+- [ ] Build `CssThemeProvider` as a real provider:
+ - Wire `useCurrentColorScheme` internally using the same defaults as `ThemeProvider`
+ (`modeStorageKey: 'mui-mode'`, `colorSchemeStorageKey: 'mui-color-scheme'`,
+ `attribute: 'data-mui-color-scheme'`) so `InitColorSchemeScript` works out of the box
+ without any extra user steps.
+ - Provide a theme context so `useTheme()` works (currently missing despite JSDoc claiming otherwise).
+ - `disableTransitionOnChange` to suppress CSS transitions during color scheme switch.
+ - `defaultMode` prop (`'light' | 'dark' | 'system'`).
+ - `noSsr` prop to skip the hydration-safe default and read localStorage on first render,
+ matching `CssVarsProvider`'s `noSsr` behavior.
+- [ ] Fix `CssVarsInjector` for nested providers: use scoped injection (not a global
+ `
+
+
+ );
+}
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..e8bec75d11c49e
--- /dev/null
+++ b/test/css-theme-provider-vite-sandbox/vite.config.ts
@@ -0,0 +1,69 @@
+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'),
+ },
+ 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..701deefaf5f4b8
--- /dev/null
+++ b/test/emotion-vite-sandbox/src/App.tsx
@@ -0,0 +1,97 @@
+/**
+ * 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: true,
+ 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..21ab7ee68ee16c
--- /dev/null
+++ b/test/emotion-vite-sandbox/vite.config.ts
@@ -0,0 +1,67 @@
+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'),
+ },
+ 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'),
+ },
+ ],
+ },
+}));
diff --git a/test/noop-vite-sandbox/README.md b/test/noop-vite-sandbox/README.md
new file mode 100644
index 00000000000000..e63d6e270ee0b0
--- /dev/null
+++ b/test/noop-vite-sandbox/README.md
@@ -0,0 +1,119 @@
+# noop-vite-sandbox
+
+A minimal Vite + React app that exercises the **non-Emotion (CSS-only) path** for
+Material UI components.
+
+## Purpose
+
+Verifies that:
+
+1. `@mui/styled-engine` can be aliased to `@mui/styled-engine-noop` with zero
+ Emotion in the output bundle.
+2. `Slider` renders correctly using `Slider.css` + `default-theme.css` (CSS
+ variables, no runtime theming).
+3. The `sx` prop fires a dev-only `console.error` and is otherwise ignored (open
+ the browser DevTools console to see it).
+4. `className`-based overrides beat `@layer mui.default` without any
+ `!important`.
+5. Dark mode works by flipping `data-mui-color-scheme="dark"` on
+ `document.documentElement` β no provider, no React context.
+
+### Why only Slider?
+
+Only components that have been converted to the CSS-base pattern are safe to use
+here. Non-converted components (e.g. `Box`, `Typography`) still contain
+theme-dependent style functions that call `useTheme()` at render time. Without a
+`ThemeProvider` in the tree, `useTheme()` returns `undefined` and the first
+`theme.palette.*` access throws, crashing the render. As more components are
+converted to CSS base styles, they can be added to this sandbox.
+
+## Dev server
+
+```bash
+pnpm -F @mui-internal/noop-vite-sandbox dev
+```
+
+Then open .
+
+## Production build
+
+```bash
+pnpm -F @mui-internal/noop-vite-sandbox build
+```
+
+Expected output (approximate):
+
+```
+dist/assets/index-*.css ~32 kB (default-theme.css + Slider.css)
+dist/assets/index-*.js ~280 kB (~90 kB gzip)
+```
+
+## Verifying zero Emotion
+
+After a production build, check that no `@emotion/*` runtime is bundled:
+
+```bash
+# Should print nothing
+grep -r "@emotion/react\|@emotion/styled\|@emotion/cache\|EmotionCacheContext\|insertStyles\|createCache" \
+ test/noop-vite-sandbox/dist/
+```
+
+The string `__emotion_real` appears once β it is a property-name guard inside
+`@mui/system/createStyled` (`style.__emotion_real === style`), not an Emotion
+package import. It is safe to ignore.
+
+## How the engine alias works
+
+`vite.config.ts` uses an **array-form alias** (order matters):
+
+```ts
+alias: [
+ // 1. Exact CSS path must come before the package catch-all
+ { find: '@mui/material/default-theme.css', replacement: 'β¦/packages/mui-material/default-theme.css' },
+ // 2. Package source (monorepo)
+ { find: '@mui/material', replacement: 'β¦/packages/mui-material/src' },
+ // 3. THE KEY SWAP β replaces Emotion with the zero-runtime noop engine
+ { find: '@mui/styled-engine', replacement: 'β¦/packages/mui-styled-engine-noop/src' },
+ // 4. Other @mui/* from monorepo source
+ β¦
+]
+```
+
+This mirrors exactly what a non-Emotion user would add to their own
+`vite.config.ts`:
+
+```ts
+// User's project vite.config.ts
+resolve: {
+ alias: {
+ '@mui/styled-engine': '@mui/styled-engine-noop',
+ },
+},
+```
+
+## 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 serves as both a live demo of the non-Emotion path and the harness for
+measuring JS bundle deltas between the Emotion and noop engines.
diff --git a/test/noop-vite-sandbox/index.html b/test/noop-vite-sandbox/index.html
new file mode 100644
index 00000000000000..a9b11af9400052
--- /dev/null
+++ b/test/noop-vite-sandbox/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ MUI noop-engine Vite sandbox
+
+
+
+
+
+
diff --git a/test/noop-vite-sandbox/package.json b/test/noop-vite-sandbox/package.json
new file mode 100644
index 00000000000000..66cfd5bb21e156
--- /dev/null
+++ b/test/noop-vite-sandbox/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "@mui-internal/noop-vite-sandbox",
+ "version": "0.0.1",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@mui/material": "workspace:*",
+ "@mui/system": "workspace:*",
+ "@mui/utils": "workspace:*",
+ "@mui/styled-engine-noop": "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/noop-vite-sandbox/src/App.tsx b/test/noop-vite-sandbox/src/App.tsx
new file mode 100644
index 00000000000000..18a2e70280f7e0
--- /dev/null
+++ b/test/noop-vite-sandbox/src/App.tsx
@@ -0,0 +1,104 @@
+/**
+ * noop-engine sandbox β Slider only.
+ *
+ * Only Slider is used here because it is the only component converted to the
+ * CSS-base pattern so far. Other components (Box, Typography, etc.) still use
+ * theme-dependent style functions that call useTheme() at render time. Without
+ * a ThemeProvider in the tree, those calls return undefined and crash the
+ * render. Stick to Slider until more components are converted.
+ *
+ * Verifies that:
+ * 1. @mui/styled-engine is aliased to @mui/styled-engine-noop (no Emotion).
+ * 2. Slider renders correctly using Slider.css + default-theme.css (CSS vars).
+ * 3. The `sx` prop fires a console.error in dev and is otherwise ignored
+ * (open the browser DevTools console to see the warning).
+ * 4. className-based overrides beat @layer mui.default without !important.
+ * 5. Dark mode works by toggling data-mui-color-scheme on .
+ *
+ * To confirm no Emotion is bundled, run:
+ * pnpm -F @mui-internal/noop-vite-sandbox build
+ * Then:
+ * grep -r "@emotion/react\|@emotion/styled\|@emotion/cache\|EmotionCacheContext" dist/
+ * # should print nothing
+ */
+import * as React from 'react';
+
+// No ThemeProvider. This CSS file provides all --mui-* custom properties.
+// Generated by: pnpm -F @mui/material build:theme-css
+import '@mui/material/default-theme.css';
+
+// Only import components that have been converted to the CSS-base pattern.
+import Slider from '@mui/material/Slider';
+
+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 (
+
+
noop-engine sandbox
+
+ Engine: @mui/styled-engine-noop β no Emotion in this bundle.
+
+
+ {/* Dark mode toggle β pure DOM attribute flip, no ThemeProvider */}
+
+
+
+
Slider value: {value}
+
+ {/*
+ sx prop intentionally passed to trigger the noop dev warning.
+ Open the browser console β you should see:
+ "MUI: The `sx` prop was used on but `@mui/styled-engine-noop` is active."
+ */}
+ setValue(v as number)}
+ // @ts-ignore β sx is valid on Slider types but deliberately passed to trigger the noop warning
+ sx={{ color: 'red' }}
+ aria-label="Sandbox slider"
+ />
+
+
+ className override (thumb should be purple, overriding the default via
+ plain CSS that beats @layer mui.default):
+
+
+ {/* className override β unlayered CSS always wins over @layer mui.default */}
+ setValue(v as number)}
+ className="custom-slider"
+ aria-label="Custom class slider"
+ />
+
+
+
+
+ );
+}
diff --git a/test/noop-vite-sandbox/src/main.tsx b/test/noop-vite-sandbox/src/main.tsx
new file mode 100644
index 00000000000000..033af594370fdc
--- /dev/null
+++ b/test/noop-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/noop-vite-sandbox/src/vite-env.d.ts b/test/noop-vite-sandbox/src/vite-env.d.ts
new file mode 100644
index 00000000000000..726765f1199e0c
--- /dev/null
+++ b/test/noop-vite-sandbox/src/vite-env.d.ts
@@ -0,0 +1,2 @@
+// Allows `import '*.css'` without TypeScript errors.
+declare module '*.css' {}
diff --git a/test/noop-vite-sandbox/tsconfig.json b/test/noop-vite-sandbox/tsconfig.json
new file mode 100644
index 00000000000000..7b1f33c4f6a88a
--- /dev/null
+++ b/test/noop-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/noop-vite-sandbox/vite.config.ts b/test/noop-vite-sandbox/vite.config.ts
new file mode 100644
index 00000000000000..a807346f97e33a
--- /dev/null
+++ b/test/noop-vite-sandbox/vite.config.ts
@@ -0,0 +1,77 @@
+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/noop-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'),
+ },
+ plugins: [
+ // @mui/material source files use .js extension but contain JSX.
+ // This plugin re-parses them as JSX so Rollup/esbuild can handle them.
+ // transformWithEsbuild must be imported at the top level β a dynamic
+ // import inside the transform hook breaks the Vite dev server.
+ // 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: [
+ // CSS entry must come before the @mui/material package alias so the
+ // catch-all doesn't rewrite it to packages/mui-material/src/default-theme.css
+ {
+ find: '@mui/material/default-theme.css',
+ replacement: path.resolve(MONOREPO_ROOT, 'packages/mui-material/default-theme.css'),
+ },
+ // 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/tsconfig.json b/tsconfig.json
index 11af6fb5ec18bb..4277b51acfe290 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -26,6 +26,10 @@
"@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/styled-engine-noop": ["./packages/mui-styled-engine-noop/src"],
+ "@mui/styled-engine-noop/*": ["./packages/mui-styled-engine-noop/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"],