Repository: material-ui
Issue: [RFC] Zero-runtime CSS-in-JS implementation
Labels: performance,package: system,RFC,package: pigment-css

### What's the problem? 🤔

This RFC is a proposal for implementing a zero-runtime CSS-in-JS solution to be used in a future major version of Material UI and Joy UI.

TLDR: We are planning to develop a custom implementation of a zero-runtime CSS-in-JS library with ideas from [Linaria](https://github.com/callstack/linaria) and [Compiled](https://compiledcssinjs.com/).

With the rising popularity of React Server Components (RSCs), it’s important that we support this new pattern for all components that are compatible. This mainly applies to layout components such as Box, Typography, etc., as they are mostly structural and the only blocker for RSC compatibility is the use of Emotion.

Another aspect is the use of themes. Currently, they need to be passed through a Provider component (especially if an application is using multiple themes) which uses React Context. RSCs do not support states/contexts.

In the last major version, we moved the styling solution to Emotion for more performant dynamic styles. Since then, Internet Explorer has been deprecated, enabling us to go all in on CSS Variables. We already use this with an optional provider ([CSS theme variables - MUI System](https://mui.com/system/experimental-api/css-theme-variables/)).

### What are the requirements? ❓

- Minimal runtime for peak performance and negligible JS bundle size as far as the runtime is concerned.
- Supporting RSC as part of the styling solution means no reliance on APIs unavailable to server components (React Context).
- Keep the same DX. You should still be able to use the `sx` prop along with container-specific props like `<Box marginTop={1} />`etc.
- It should be possible to swap out the underlying CSS-in-JS pre-processor. We have already explored using `emotion` as well as `stitches`, as mentioned below.
- Source map support. Clicking on the class name in the browser DevTools should take you to the style definitions in the JS/TS files.
- Minimal breaking changes for easier migration.

### What are our options? 💡

We went through some of the existing zero-runtime solutions to see if they satisfy the above requirements.

1. [vanilla-extract](https://vanilla-extract.style/) - This ticks most of the boxes, especially when used along with libraries like [dessert-box](https://github.com/TheMightyPenguin/dessert-box/). But its limitation to only be able to declare styles in a `.css.ts` file felt like a negative point in DX.
2. [Compiled](https://compiledcssinjs.com/) - Compiled is a CSS-in-JS library that tries to follow the same API as Emotion which seems like a win, but it has some cons:
    - Theming is not supported out of the box, and there’s no way to declare global styles.
    - Atomic by default. No option to switch between atomic mode and normal CSS mode.
3. [Linaria](https://github.com/callstack/linaria) - Linaria in its default form only supports CSS declaration in tagged template literals. This, along with no theming support as well as no way to support the `sx` prop led us to pass on Linaria.
4. [PandaCSS](https://panda-css.com/) - PandaCSS supports all the things that we require: a `styled` function, Box props, and an equivalent of the  `sx` prop. The major drawback, however, is that this is a PostCSS plugin, which means that it does not modify the source code in place, so you still end up with a not-so-small runtime (generated using `panda codegen`) depending on the number of features you are using. Although we can’t directly use PandaCSS, we did find that it uses some cool libraries, such as `ts-morph` and `ts-evaluate` to parse and evaluate the CSS in its [extractor](https://github.com/chakra-ui/panda/tree/main/packages/extractor) package.
5. [UnoCSS](https://unocss.dev/) - Probably the fastest since it does not do AST parsing and code modification. It only generates the final CSS file. Using this would probably be the most drastic and would also introduce the most breaking changes since it’s an atomic CSS generation engine. We can’t have the same `styled()` API that we know and love. This would be the least preferred option for Material UI, especially given the way our components have been authored so far.

Although we initially passed on Linaria, on further code inspection, it came out as a probable winner because of its concept of external [tag processors](https://github.com/callstack/linaria/blob/master/docs/CONFIGURATION.md#:~:text=%7D%0A%5D%3B-,tagResolver,-%3A%20(source%2C%20tag)%20%3D%3E%20string). If we were to provide our own tag processors, we would be able to support CSS object syntax as well as use any runtime CSS-in-JS library to generate the actual CSS. So we explored further and came up with two implementations:

1. [emotion](https://github.com/brijeshb42/material-ui/tree/system-zero-runtime/packages/mui-linaria-processor) - The CSS-in-JS engine used to generate the CSS. This [Next.js app router example](https://github.com/brijeshb42/next-app-linaria/blob/main/src/app/page.tsx) is a cool demo showcasing multiple themes with server actions.
2. [no-stitches](https://github.com/brijeshb42/material-ui/tree/system-zero-runtime/packages/mui-no-stitches) - Supports the `styled` API from [Stitches](https://stitches.dev/). See this [discussion](https://github.com/stitchesjs/stitches/discussions/1149#discussioncomment-6383912) for the final result of the exploration.

The main blocker for using Linaria is that it does not directly parse the JSX props that we absolutely need for minimal breaking changes. That meant no direct CSS props like `<Box marginTop={1} />` or `sx` props unless we converted it to be something like `<Component sx={sx({ color: 'red', marginTop: 1 })} />`. (Note the use of an `sx` function as well.) This would enable us to transform this to `<Component sx="random-class" />` at build-time, at the expense of a slightly degraded DX.

### Proposed solution 🟢

So far, we have arrived at the conclusion that a combination of `compiled` and `linaria` should allow us to replace `styled` calls as well as the `sx` and other props on components at build time. So we’ll probably derive ideas from both libraries and combine them to produce a combination of packages to extract AST nodes and generate the final CSS per file. We’ll also provide a way to configure prominent build tools (notably Next.js and Vite initially) to support it.

### Theming

Instead of being part of the runtime, themes will move to the config declaration and will be passed to the `styled` or `css` function calls. We’ll be able to support the same theme structure that you know created using `createTheme` from `@mui/material`.

To access theme(s) in your code, you can follow the callback signature of the `styled` API or the `sx` prop:

```tsx
const Component = styled('div')(({ theme }) => ({
  color: theme.palette.primary.main,
  // ... rest of the styles
}))
// or
<Component sx={({ theme }) => ({ backgroundColor: theme.palette.primary... })} />
```

Although theme tokens’ structure and usage won’t change, one breaking change here would be with the `component` key. The structure would be the same, except the values will need to be serializable.

Right now, you could use something like:

```tsx
const theme = createTheme({
  components: {
    // Name of the component
    MuiButtonBase: {
      defaultProps: {
        // The props to change the default for.
        disableRipple: true,
        onClick() {
          // Handle click on all the Buttons.
	}
      },
    },
  },
});
```

But with themes moving to build-time config, `onClick` won’t be able to be transferred to the Button prop as it’s not serializable. Also, a change in the `styleOverrides` key would be required not to use `ownerState` or any other prop values. Instead, you can rely on the `variants` key to generate and apply variant-specific styles

#### Before

```js
const theme = createTheme({
  components: {
    MuiButton: {
      styleOverrides: {
        root: ({ ownerState }) => ({
          ...(ownerState.variant === 'contained' &&
            ownerState.color === 'primary' && {
              backgroundColor: '#202020',
              color: '#fff',
            }),
        }),
      },
    },
  },
});
```

#### After

```js
const theme = createTheme({
  components: {
    MuiButton: {
      variants: [
        {
          props: { variant: 'contained', color: 'primary' },
          style: {
            backgroundColor: '#202020',
            color: '#fff'
          },
        },
      ],
    },
  },
});
```

### Proposed API

The `styled` API will continue to be the same and support both CSS objects as well as tagged template literals. However, the `theme` object will only be available through the callback signature, instead of being imported from a local module or from `@mui/material` :

```tsx
// Note the support for variants
const Component = styled('div')({
  color: "black",
  variants: {
    size: {
      small: {
	fontSize: '0.9rem',
	margin: 10
      },
      medium: {
        fontSize: '1rem',
	margin: 15
      },
      large: {
	fontSize: '1.2rem',
	margin: 20
      },
    }
  },
  defaultVariants: {
    size: "medium"
  }
})
// Or: 
const ColorComponent = styled('div')(({ theme }) => ({
  color: theme.palette.primary.main
});
```

The `theme` object above is passed through the bundler config. At build-time, this component would be transformed to something like that below (tentative):

```tsx
const Component = styled('div')({
  className: 'generated-class-name',
  variants: {
    size: {
      small: "generated-size-small-class-name",
      medium: "generated-size-medium-class-name",
      large: "generated-size-large-class-name",
    }
  }
});
/* Generated CSS:
.generated-class-name {
  color: black;
}
.generated-size-small-class-name {
  font-size: 0.9rem;
  margin: 10px;
}
.generated-size-medium-class-name {
  font-size: 1rem;
  margin: 15px;
}
.generated-size-large-class-name {
  font-size: 1.2rem;
  margin: 20px;
}
*/
```

Dynamic styles that depend on the component props will be provided using CSS variables with a similar callback signature. The underlying component needs to be able to accept both `className` and `style` props:

```tsx
const Component = styled('div')({
  color: (props) => props.variant === "success" ? "blue" : "red",
});

// Converts to:
const Component = styled('div')({
  className: 'some-generated-class',
  vars: ['generated-var-name']
})

// Generated CSS:
.some-generated-class {
  color: var(--generated-var-name);
}

// Bundled JS:
const fn1 = (props) => props.variant === "success" ? "blue" : "red"

<Component style={{"--random-var-name": fn1(props)}} />
```

Other top-level APIs would be:

1. `css` to generate CSS classes outside of a component,
2. `globalCss` to generate and add global styles. You could also directly use CSS files as most of the modern bundlers support it, instead of using `globalCss`.
3. `keyframes` to generate scoped keyframe names to be used in animations.


### Alternative implementation

An alternative, having no breaking changes and allowing for easy migration to the next major version of `@mui/material` is to have an opt-in config package, say, for example, `@mui/styled-vite` or `@mui/styled-next`. If users don’t use these packages in their bundler, then they’ll continue to use the Emotion-based Material UI that still won’t support RSC. But if they add this config to their bundler, their code will be parsed and, wherever possible, transformed at build time. Any static CSS will be extracted with reference to the CSS class names in the JS bundles. An example config change for Vite could look like this:

```tsx
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// abstracted plugin for vite
import styledPlugin from "@mui-styled/vite";
import { createTheme } from "@mui/material/styles";

const customTheme = createTheme({
  palette: {
    primary: {
      main: '#1976d2',
    },
  },
  components: {
    MuiIcon: {
      styleOverrides: {
        root: {
          boxSizing: 'content-box',
          padding: 3,
          fontSize: '1.125rem',
        },
      },
    },
  }
  // ... other customizations that are mainly static values
});

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
  plugins: [
    styledPlugin({
      theme: customTheme,
      // ... other tentative configuration options
    }),
    react(),
  ]
}));
```

> For component libraries built on top of Material UI, none of the above changes would affect how the components are authored, except for the need to make it explicit to users about their `theme` object (if any), and how that should be imported and passed to the bundler config as discussed above.

### Known downsides of the first proposal

Material UI will no longer be a `just install-and-use` library: This is one of the features of Material UI right now. But with the changing landscape, we need to compromise on this. Several other component libraries follow a similar approach.
Depending on the bundler being used, you’ll need to modify the build config(`next.config.js` for Next.js, `vite.config.ts` for Vite, etc.) to support this. What we can do is provide an abstraction so that the changes you need to add to the config are minimal.

### Resources and benchmarks 🔗

Playground apps - 

1. [Next.js](https://github.com/mui/material-ui/tree/master/apps/zero-runtime-next-app)
2. [Vite](https://github.com/mui/material-ui/tree/master/apps/zero-runtime-vite-app)

Related issue(s)

* https://github.com/mui/material-ui/issues/34826