Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[RFC] Zero-runtime CSS-in-JS implementation #38137

Open
Assignees
brijeshb42
Labels
RFCRequest For Comments.package: pigment-cssSpecific to Pigment CSS.performancescope: systemThe system, the design tokens / styling foundations used across components. eg. @mui/system with MUI
@brijeshb42

Description

@brijeshb42

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 fromLinaria andCompiled.

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).

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 thesx 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 usingemotion as well asstitches, 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 - This ticks most of the boxes, especially when used along with libraries likedessert-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 - 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 - 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 thesx prop led us to pass on Linaria.
  4. PandaCSS - PandaCSS supports all the things that we require: astyled function, Box props, and an equivalent of thesx 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 usingpanda 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 asts-morph andts-evaluate to parse and evaluate the CSS in itsextractor package.
  5. UnoCSS - 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 samestyled() 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 externaltag processors. 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 - The CSS-in-JS engine used to generate the CSS. ThisNext.js app router example is a cool demo showcasing multiple themes with server actions.
  2. no-stitches - Supports thestyled API fromStitches. See thisdiscussion 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} /> orsx props unless we converted it to be something like<Component sx={sx({ color: 'red', marginTop: 1 })} />. (Note the use of ansx 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 ofcompiled andlinaria should allow us to replacestyled calls as well as thesx 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 thestyled orcss function calls. We’ll be able to support the same theme structure that you know created usingcreateTheme from@mui/material.

To access theme(s) in your code, you can follow the callback signature of thestyled API or thesx prop:

constComponent=styled('div')(({ theme})=>({color:theme.palette.primary.main,// ... rest of the styles}))// or<Componentsx={({ theme})=>({backgroundColor:theme.palette.primary...})}/>

Although theme tokens’ structure and usage won’t change, one breaking change here would be with thecomponent key. The structure would be the same, except the values will need to be serializable.

Right now, you could use something like:

consttheme=createTheme({components:{// Name of the componentMuiButtonBase:{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 thestyleOverrides key would be required not to useownerState or any other prop values. Instead, you can rely on thevariants key to generate and apply variant-specific styles

Before

consttheme=createTheme({components:{MuiButton:{styleOverrides:{root:({ ownerState})=>({          ...(ownerState.variant==='contained'&&ownerState.color==='primary'&&{backgroundColor:'#202020',color:'#fff',}),}),},},},});

After

consttheme=createTheme({components:{MuiButton:{variants:[{props:{variant:'contained',color:'primary'},style:{backgroundColor:'#202020',color:'#fff'},},],},},});

Proposed API

Thestyled API will continue to be the same and support both CSS objects as well as tagged template literals. However, thetheme object will only be available through the callback signature, instead of being imported from a local module or from@mui/material :

// Note the support for variantsconstComponent=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:constColorComponent=styled('div')(({ theme})=>({color:theme.palette.primary.main});

Thetheme object above is passed through the bundler config. At build-time, this component would be transformed to something like that below (tentative):

constComponent=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 bothclassName andstyle props:

constComponent=styled('div')({color:(props)=>props.variant==="success" ?"blue" :"red",});// Converts to:constComponent=styled('div')({className:'some-generated-class',vars:['generated-var-name']})// Generated CSS:.some-generated-class{color:var(--generated-var-name);}// Bundled JS:constfn1=(props)=>props.variant==="success" ?"blue" :"red"<Componentstyle={{"--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 usingglobalCss.
  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:

import{defineConfig}from"vite";importreactfrom"@vitejs/plugin-react";// abstracted plugin for viteimportstyledPluginfrom"@mui-styled/vite";import{createTheme}from"@mui/material/styles";constcustomTheme=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/exportdefaultdefineConfig(({ 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 theirtheme 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 ajust 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
  2. Vite

Related issue(s)

Metadata

Metadata

Assignees

Labels

RFCRequest For Comments.package: pigment-cssSpecific to Pigment CSS.performancescope: systemThe system, the design tokens / styling foundations used across components. eg. @mui/system with MUI

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions


    [8]ページ先頭

    ©2009-2025 Movatter.jp