Open brijeshb42 opened 9 months ago
<Component sx={({ theme }) => ({ backgroundColor: theme.palette.primary... })} />
I'm a bit concerned about how the theme is accessed. Right now, if Component
is a client component while the parent is a server component, we can't access the theme like this because functions are not serializable.
@mwskwong We should use CSS vars. A lot faster and easier to write. We'll miss the typecheck, but we'll have to live with that.
<Component sx={{ backgroundColor: 'primary' }} />
@mwskwong This is what you write in your code which will then be replaced at build time (dev or prod) with the generated css. theme
will be callback argument passed by the said tool to your function. It won't matter in that case whether it's a client or a server component as far as the sx
prop is concerned.
Nice! I wonder how nested themes would be supported š¤.
Our use case: We heavily use nested themes where our main theme overwrites almost every component. For specific pages we have a unique look and feel where we use nested themes that partially update some components (ex font family for all h1-h4 or fully rounded buttons).
Another use case: we have a fully dark based theme except for our ecommerce pages they are completely light theme based (again with a nested light theme), but the header and footer stay in the dark theme for example š .
Looking forward! Regards
hey, this looks amazing !
regarding Panda, I wanted to add a little more infos:
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.
I can understand that. You could evaluate those css
calls etc at build-time I guess tho, as it was done by the Qwik people here
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 package.
you could use the extractor on its own if that helps, it has no panda-specific dependencies !
This could be a brave idea, but what do you think about completely dropping or making opt-in CSS-IN-JS in favor of libraries like TailwindCSS or UnoCSS for styling?
It could be an ambitious, but long-term win solution and definitely a win-win decision
No matter what you will do, you will have to implement an intermediate layer.
One question: Is CSS-in-JS support incoming for RSC on the React side? There has been some discussion around this, though I can't find where.
One question: Is CSS-in-JS support incoming for RSC on the React side? There has been some discussion around this, though I can't find where.
The reason why runtime CSS-in-JS (not just CSS in JS in general to be exact) has so many problems in the new architecture of React is because as its name suggests, it needs JS in runtime to function, while RSC does the exact opposite.
For this RFC, we are talking about zero runtime CSS-in-JS, so ideally, everything will be converted into plain CSS during built-time and won't have the issue we are facing. Although one thing to consider is whether it can maintain a certain degree of dynamic since we no longer have access to resources like theme during runtime.
Why not use CSS Modules for theming ? I believe this would be simpler and easier to do. Though the dynamic styling part needs to be figured out. Maybe use good old styles object with CSSProperties
from react for type safety.
We recently diteched our old DS which used context and CSS-IN-JS for theming and the new CSS Variables are way easier to do with Design tokens as well. Performant and can easily style components if required. Can also do Module Scss if you would like.
We took alot of inspiration from MUI the way its built and the components API as well. Thanks for building this amazing Library.
@mwskwong We should use CSS vars. A lot faster and easier to write. We'll miss the typecheck, but we'll have to live with that.
The situation I was mentioning can also appear to the new CssVarsProvider
API, which is using CSS variables. e.g.
<List size="sm" sx={{ "--List-radius": theme => theme.vars.radius.sm }} />
Such a way of accessing the radius CSS var is more scalable. And yes, I can also do "--List-radius": "var(--joy-radius-sm)"
(which is what I'm doing for the sake of RSC compatibility), but I'm taking the risk of making typos.
@mwskwong This is what you write in your code which will then be replaced at build time (dev or prod) with the generated css.
theme
will be callback argument passed by the said tool to your function. It won't matter in that case whether it's a client or a server component as far as thesx
prop is concerned.
Thanks @brijeshb42 on elaborate on that part, I would say that will be the best of both worlds.
Another use case: we have a fully dark based theme except for our ecommerce pages they are completely light theme based (again with a nested light theme), but the header and footer stay in the dark theme for example š .
@JanStevens, we can support this by injecting CSS variables wherever we had previously nested ThemeProvider
components. The theme structure would be the same, the usage would be the same, and only the CSS variables' values would change.
Another use case: we have a fully dark based theme except for our ecommerce pages they are completely light theme based (again with a nested light theme), but the header and footer stay in the dark theme for example š .
@JanStevens, we can support this by injecting CSS variables wherever we had previously nested
ThemeProvider
components. The theme structure would be the same, the usage would be the same, and only the CSS variables' values would change.
Just wondering, with the introduction of data-mui-color-scheme="dark"
(or light
), does nested ThemeProvider
still need to be supported?
@mwskwong We might still keep the ThemeProvider
component as-is for compatibility but it'll be replaced at build time with a simple div
or as
prop. We are still exploring the finer details.
@astahmer I did explore using @pandacss/extractor
package and even had a working demo. But later I found that ts-evaluator
is not very foolproof compared to how Linaria evaluates values (using node:module
). That led us to ditch the whole package along with ts-morph
. I feel the extraction part itself is simpler but the main part of the implementation resides in how we can evaluate the extracted AST nodes.
One question: Is CSS-in-JS support incoming for RSC on the React side? There has been some discussion around this, though I can't find where.
The reason why runtime CSS-in-JS (not just CSS in JS in general to be exact) has so many problems in the new architecture of React is because as its name suggests, it needs JS in runtime to function, while RSC does the exact opposite.
For this RFC, we are talking about zero runtime CSS-in-JS, so ideally, everything will be converted into plain CSS during built-time and won't have the issue we are facing. Although one thing to consider is whether it can maintain a certain degree of dynamic since we no longer have access to resources like theme during runtime.
Having no JS after compilation will be a major upgrade for MUI (YES! less JS shipped is always a good thing š), I hope you guys still support sx
cause without it life gonna pretty hard!
@SC0d3r Yes. We'll still have sx
prop. See our exploratory POC.
Going to just brain dump alot of thoughts about Linaria and Styled-Components after many many years of using them before going full circle back to SCSS for the last 1.5 years and never looking back.
Linaria
Converting to/from linaria, styled-components/emotion and css modules is easier than people might think and because it's mostly based on patterns I'd say it would be possible to code-mod most of it or do an incremental approach 1 component at a time.
Variables to/from
// in
Button.styles.ts
export const Button= styled.button`
background-color: ${(p) => p.theme.variants.secondary.backgroundColor};
`;
// out .scss
.button {
background-color: var(--secondary-background-color);
}
Modifers to/from
// Button.styles.ts
export const Button= styled.button`
background-color: ${(p) => p.theme.variants.secondary.backgroundColor};
{({primary}) => {
return css`color: red;`
}
`;
// out .scss
.button {
background-color: var(--secondary-background-color);
&--primary {
color: red;
}
}
// Button.tsx
return <button className={cx(styles.button, theme === "primary" && styles.buttonPrimary)}>{children}<button>;
Loops to/from - Just use scss loops. Reusable snippets to/from - Just use mixins
=== Final Thoughts ===
This may not be completely feasible with the MUI codebase but thought I'd share my long experience with Linaria to allow you to avoid potential headaches where possible.
Our overall UI library and application architecture was much different than MUI with less configurability so while this was the best choice for us for the reasons above it may still be a good option for you.
Will leave this here just to prove I was participating in Linaria community 4+ years ago https://github.com/callstack/linaria/issues/501
Panda-css is amazing, I did some tests when I was looking for a lib that would solve the css-in-js problem. However it is in its first releases and I want to see what else they are planning but I can say that I trust the team at Chakra-ui a lot.
Everything sounds great though i do have a major concern (or a question) here.
Lets say i have a Design System library that wraps MUI, we don't have a build process other than passing our code via babel cli for both cjs and esm transpilations.
My concern/question is, Do we now forced to use a bundler or any other tool or framework to be able to convert the styles in build time?
The 2 possible solutions i see here, and i might be way off:
@sag1v We've already covered this in the RFC
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 (if the user has included the zero runtime config in their bundler setup).
But later I found that
ts-evaluator
is not very foolproof compared to how Linaria evaluates values (usingnode:module
).
seems like everything that is pure can be compiled away down to css classes, and everything that has some form of conditional or complex js needs to either remain with some small runtime or compile down to something that looks like a classnames
/clsx
style. Just looking for recognized strings is basically what the tailwind compiler does to generate the classes.
// pure can be compiled down to classes
sx={{ padding: 3, color: 'red' }} => className="p-3px red"
// dynamic needs to retain some runtime
sx={{ padding: 3, color: isActive ? 'red' : 'blue' }} => className={classNames("p-3px", isActive ? 'red' : 'blue')}
So the strings/numbers become the important thing to interpret and then any complex code is given up on and left inline which leaves some runtime code but I don't know any way around it. The problem with inline code is always that it needs to be interpreted which is why vanilla extract just evals to css and leaves the dynamicism to their sprinkles js runtime.
I was wondering... Instead of creating MUI's own zero-runtime CSS-in-JS solution, what about helping PandaCSS migrate from PostCSS to LightningCSS? MUI spends less time re-inventing the wheel and makes a great, existing CSS solution even better.
@kylemh As explained in the RFC about panda, it's not about whether panda is using postcss or lightningcss to generate the css. Panda does not modify the original code. Which means whatever you write in your code along with how panda generates the classnames remain part of the final build and hence increase the overall bundle size together with its runtime.
Interesting. Doesn't that mean their claim to zero runtime is incorrect?
Also, another question... Will the solution MUI build be integrated as part of the monorepo or as a separate repository? If somebody wanted to use the CSS-in-JS library, will the relevant APIs be code-splittable?
It's zero-runtime in the sense that there's no runtime similar to emotion that generates dynamic stylesheets and adds it to the head
.
Will the solution MUI build be integrated as part of the monorepo or as a separate repository? If somebody wanted to use the CSS-in-JS library, will the relevant APIs be code-splitable?
@kylemh the monorepo contains different packages that can be used independently. Any package you don't use won't be included in your bundle. It is true even now, for by e.g. using @mui/system, you are not getting any of the @mui/material (Material UI)'s code.
A question about the theming that I don't believe has been answered above.
How does it affect the "automatic theme string resolving"? (I don't know what you call it officially) ie:
<Box sx={{ color: 'grey.200' }}>This is theme.palette.gray['200']</Box>
<Box sx={{ color: 'accent.main' }}>This is a custom theme color theme.palette.accent.main</Box>
First, you provide your theme
object through the bundler config. That way, this tool will have access to your theme with all it's customisations.
Then after static extraction and evaluation, we'll get the css object {color: 'grey.200'}
. This css object will then go through the same process that it goes through right now (in runtime mode) except all of this will happen at build time. See exploration code.
After processing, your code will be modified to -
<Box sx="generated-class-name-1" />
<Box sx="generated-class-name-2" />
.generated-class-name-1 {
color: actualvalueofgrey200;
}
.generated-class-name-2 {
color: actualvalueofaccentmain;
}
The underlying Box
component will just have to pass the value of sx
as className
to the finally rendered div
or whatever HTMLElement provided through as
prop.
Great work on the existing challenges and alternatives. The solution you propose seems sensible, and can be integrated downstream to dependent projects like react-admin without too much hassle (even though it'll require a major version bump).
I can't wait to see a POC on a small subset of the MUI components.
All green for me!
A question about the theming that I don't believe has been answered above.
How does it affect the "automatic theme string resolving"? (I don't know what you call it officially) ie:
<Box sx={{ color: 'grey.200' }}>This is theme.palette.gray['200']</Box> <Box sx={{ color: 'accent.main' }}>This is a custom theme color theme.palette.accent.main</Box>
If extracting the real color is hard it would also be possible to use css custom properties (css variables) for that. e.g.:
<Box sx="generated-class-name-1" style={{ "--generated-css-var-name-1": theme("grey.200") }} />
<Box sx="generated-class-name-1" style={{ "--generated-css-var-name-1": theme("actualvalueofaccentmain") }} />
.generated-class-name-1 {
color: var(generated-css-var-name-1);
}
perhaps static colors are a bad example but maybe this might help in more dynamic cases
Great job MUI team! I love where this is going, I'm just afraid that 0 runtime will limit the possiblities regarding computed values and auto-bound theme shordtcut (such as <Box m={2} />
translating to margin: 16px
with default 8px spacing).
Is it plan to also support responsive value object such as <Box m={{ xs: 2, md: 4, xl: 6 }}>
.
Great job MUI team! I love where this is going, I'm just afraid that 0 runtime will limit the possiblities regarding computed values and auto-bound theme shordtcut (such as
translating to margin: 16px with default 8px spacing).
We plan to keep supporting this.
Is it plan to also support responsive value object such as <Box m={{ xs: 2, md: 4, xl: 6 }}>.
Yes, this will be supported too.
There is a zero-runtime styling solution already, it's called CSSššš truly 0 runtime, no JS execution at all
It would be nice to see some article or docs on how to build your costom theme with custom design system on top of Mui using plain css/sass/less and using only global css selectors. For people who don't like the core idea of CSS-in-JS and delivering styles packed inside js this can be as second breath to improving performance and caching even further.
As author of Griffel and issue-contributor to Linaria let me comment on extraction/evaluator part. This conversation happened before with MUI team, but it would be great to have it public š±
Basically there are two approaches to handle style transforms: static and runtime.
Static is what used in CompiledCSS & Stylex and relies on AST analysis to extract styles. That's clean, fast and nice, but the following scenario becomes impossible:
import { vars } from './vars'
const classes = css`
color: vars.bgColor,
`
In this scenario it's not enough to analyze the source file as there is an import to ./vars
. While style definitions are usually have the strict structure, the contents of other files are a complete rabbit hole. For example, vars
variable can use third party library:
// vars.ts
import colors from 'some-colors-lib'
export const vars = {
bgColor: colors.generate().hex(),
}
As colors.generate()
is a function it becomes impossible to resolve it using only AST.
We write a lot of code in Compiled to statically evaluate CSS. And I mean a lot of code. I don't think we can reliably continue down this to get worthwhile ROI for it, even though it "feels best", as there are countless edge cases we need to handle, and will need to handle in the future as the library rolls out. What's worse is its not portable, so if we were to keep investing in this area, when we move to a newer bundler (e.g. esbuild) we'd have to throw the code away (or re-use it inside esbuild, but that would only be an interim solution).
Runtime is what is used in Griffel, Linaria and Vanilla Extract. Griffel relies on Linaria evaluation and Vanilla Extract uses vite-node.
Approaches are bit different, but the goal is the same: resolve expressions from styles using Node runtime and place results back in AST. In the scenario above the function colors.generate().hex()
will be invoked and resolved to a real value.
This also allows to have the same code for runtime & transforms (AST => values => JS code => AST).
The caveats are performance (basically, it's a bundler inside a bundler) and unsafe code for Node (https://github.com/vitest-dev/vitest/issues/3525). Linaria is in a better position as it's designed for this purpose and also avoids evaluation of unrelated code.
You can check the reference implementation of Griffel processor for Linaria v4 there (https://github.com/microsoft/griffel/pull/414).
The hard truth is that I am still working with @Anber to push Linaria v4 into the state when it can compile Microsoft projects šæ
The final decision on the approach depends on the final API, but it's good to consider possible limitations.
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.
It's not entirely true. At present, Linaria serves more as an engine for zero-runtime evaluations rather than solely functioning as a CSS-in-JS library. The modules @linaria/core
and @linaria/styled
are considered reference implementations. Additionally, as mentioned by @layershifter, they have developed their own CSS-in-JS library with a distinct syntax, which is actually built upon the Linaria.
@Anber I completely agree that it can serve as a building block for another library. I've explored this in the RFC as well.
The problem while evaluating using Linaria was that with tagged template literals, Linaria already did the process of hoisting the inlined function expression and that was then taken over by the relevant styled processor (@linaria/react/processors
). But the same would not work with css object -
const H1 = styled.h1({
color: (props) => props.isRed ? 'red' : 'blue',
});
but would work with
const H1 = styled.h1`
color: ${(props) => props.isRed ? 'red' : 'blue'};
`
So essentially, I will not be able to support dynamic styles in the same way it can be done with template literals. Please correct me if I am missing something.
@brijeshb42 https://github.com/callstack/linaria/blob/master/packages/testkit/src/babel.test.ts#L2744 like this? :)
@Anber Not really. I was able to make something similar work. See this. I was asking how dynamic styles would work when thought of in the same way how Linaria hoists expressions in template literals.
@brijeshb42 The implementation depends on the nature of the theme
object.
If the theme
object is a simple recursive one with string keys and primitive values, references like theme.colors.primaryMain.value
can be replaced with CSS variables like var(--theme-colors-primaryMain-value)
. In this case, it comes down to makeStyles
.
However, if the theme
object can take any form, or if a value from the theme needs to be modified in a template (e.g., hslToRgb(theme.colors.primaryMain.value)
), it's better to define locally scoped variables inside the component. Consequently, your SliderRoot
will be compiled to something like this:
const SliderRoot = themedStyled("span", {
// ā¦
class: ".extracted-class-name",
theme: {
"--PREFIX-theme-colors-primaryMain-value": theme => theme.colors.primaryMain.value,
"--PREFIX-theme-colors-secondaryMain-value": theme => theme.colors.secondaryMain.value,
}
})
@brijeshb42 Would you mind adding links, sandbox, or docs to play with the POC?
@siriwatknp The POC is within the apps
folder. There are two apps. One using nextjs and other using vite. There's README in both the apps explaining how to get started.
I feel like it's not super clear why the team behind Material UI is working on a new styling solution. We have https://mui-org.notion.site/pigment-css-Why-a-standalone-project-25205c31c8564e0daee6f79523de9b5c#67fa15ea6a03492ba92ac299f22ef98e where we gather all the challenges with existing solutions.
Here is how I understand the situation:
Performance / Scaling issue https://mui-org.notion.site/pigment-css-Why-a-standalone-project-25205c31c8564e0daee6f79523de9b5c#67fa15ea6a03492ba92ac299f22ef98e
Atomic class names. Because of the atomic class name output, it's a real challenge for developers who use components built on top of Tailwind CSS to customize them at scale. I think it's why Shadcn UI (and Tailwind UI before) resonates with developers (no npm package) while NextUI doesn't (npm package). How can you customize a component built with TailwindĀ CSS? It's worse than Material UI. But if all the styles are in your codebase, then it becomes easier. So I believe TailwindĀ CSS makes it easier for developers who create design systems to customize things because they can remove styles (which I think Material UI should learn from, offer a version of Material UI built on top of Base UI where all the styles are not in an npm package) but harder for developers who then need to customize these Tailwind CSS based components.
So it's about being able to debug in dev tools without access to the source, styles are spread across many atomic classes.
=> We need a solution that outputs a single clear class name. Material UI v4 was the dream on this front IMHO.
Unlearning CSS. Tailwind CSS API is not CSS, people have to learn a new API, on top of learning CSS to truly understand what they are doing. This is friction (there is definitely value in having a strong default but doesn't need this API). For example, I don't want to have to memorize https://tailwindcomponents.com/cheatsheet/.
Also, if we can trust https://2023.stateofcss.com/en-US/css-frameworks/, there is a plateau of adoption:
=> We need a solution where we can use all the most advanced CSS features, and where people who know CSS can be productive with the least possible learning curve. I believe the sx
prop and the styled()
API strike this great balance.
Migration. JSS -> styled() was a painful migration. We already plan to support Tailwind CSS with Base UI and a version of it with ready-to-use styles (per my point before, Material UI with an npm package with Tailwind CSS makes little sense IMHO). We are working toward having each demo of Base UI have a Tailwind CSS option and an easy CLI to copy the source. Today, this looks like this: https://mui.com/base-ui/react-button/#introduction (we can still do much better).
=> For Material UI, we need the new API to be backward compatible with Emotion.
Bundle size. Panda CSS keeps the original style in the JavaScript bundle, while this is one of the most expensive resources. The classic https://medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4 (which has had multiple updates for 2022, and 2023).
=> Transform the source to remove the style.
Subset of CSS. For example, nested selectors aren't truly possible: https://stylexjs.com/blog/introducing-stylex#what-is-stylex. I don't see how this is practical, but it was a design decision.
=> The API needs to be a superset of CSS.
Perfomance. See the mention for the React tech lead: https://github.com/reactwg/react-18/discussions/108
Our preferred solution is to use https://github.com/reactwg/react-18/discussions/108 for statically extracted styles and plain inline styles for dynamic values.
And there are fields that that support it: https://pustelto.com/blog/css-vs-css-in-js-perf/.
=> We moved from a more static solution, JSS, to Emotion in 2020 to benefit from dynamic properties. CSS variables are now supported enough to be that solution. So we should be able to take a step back to where Material UI v4 was at in terms of tradeoff.
RSC. For static content, we don't need to send JavaScript client side.
=> Static extract is one solution. To be fair, it's not the only one. I think the support for this could land in Emotion in the near future. It seems to me that we could use the React cache
API with React 18 today to replace the React context in the server, it's scoped to the request with Next.js: https://github.com/manvalls/server-only-context. @Andarist is exploring https://github.com/emotion-js/emotion/issues/2928#issuecomment-1902791219 the use of React 19, <link>
API https://github.com/facebook/react/pull/25243/files#diff-9d933ceb7f980ff2bf51047ef3937bcd1bf172f3fffc8290232c7634334f2095R790.
No strong concern, I believe our current solution is to package it in a way that is fully consumable with existing emotion/styled-components APIs for a smooth migration story.
So we could say, we use Linaria. The documentation experience isn't great though, š , I could see ourselves creating a lot of it.
I believe we solve all the issues raised in https://dev.to/srmagura/why-were-breaking-up-wiht-css-in-js-4g9b.
Our current progress: https://github.com/mui/material-ui/pull/40685.
@oliviertassinari you mentioned about Atomic CSS
:
=> We need a solution that outputs a single clear class name. Material UI v4 was the dream on this front IMHO.
In both, tokenami
and stylex
, the maintainers are open to discussing new API (ex. outputting a constant class name):
The constant class name feature is a simple extra parameter (ex. object to accommodate all other possible requests with name
property) that stylex could accept and toggled on and off with a bundling pluginās option.
Whereas nested CSS selectors rule could also be moved to the bundling plugin to enforce and toggled on and off.
Why not to consider to touch base with stylex
maintainers (ex. @nmn) and request what is missing for MaterialUI.
Itād be a win-win for everyone.
I would love to know the reasoning against atomic styles. The only concern that I saw is that with atomic styles like Tailwind, it becomes hard to customise styles. This is not the case with StyleX. One of the biggest reasons for StyleX existing is reliably style merging across components so that design system components can be predictably customised. FWIW, Panda does pretty well on this metric as well.
I think it would be useful to consider generating atomic styles because they actually give you more reliable style merging than traditional CSS.
Now, to address the particulars of using StyleX:
StyleX does not support the "styled components" API with a string to define styles and we don't plan to. However, it should be easy to combine StyleX with a separate Babel transform that supports the API you want. I've published a similar package, tw-to-stylex
that compiles Tailwind to stylex calls.
group
from Tailwind.Next I want to chime in and say that other than StyleX, I would also take a look at Panda, Griffel and Linaria. I'm less sure about Tokenami, because although the actual CSS is in a static file, it relies on heavy usage of CSS variables within inline styles. I'm not sure if that will suffer from the same performance issues as inline styles or not.
@layershifter also made the following comment:
but the following scenario becomes impossible:
import { vars } from './vars'
const classes = css`
color: vars.bgColor,
`
We have found a way to support a pattern similar to this with our theming APIs, but he's right that it doesn't allow arbitrarily importing values across files and using them.
From the video Dream CSS and JS Solution (4:20), the main problem presented against StyleX boils down to "for me it's a dealbreaker because I like nested selector", to be fair it's your project and your decisions, but it does not sound like a tech reason.
About PandaCSS generating only atomic classes, as other people said here, I wonder why not contribute to the project, it seems like it could be achieved there, and a new option like atomicClass: false
could be handy for other people. I like components as selectors too, but it feels much more like a debug option to find your component based on class name, following this idea this specific class name is not needed in the build time, for that you can still rely on runtime.
I understand it could generate friction due to slow decisions cross-project and a lack of freedom to make decisions for MUI itself, which sounds like the main reason behind the whole move of creating a whole new style engine.
IMOP, It is a non-starter if the final solution is vendor lock-in (e.g. requiring developers to use babel/webpack). AFAIK a lot of zero-runtime CSS-in-JS solution requires a compiler, and that compiler usually only being implemented in one ecosystem (as a babel plugin or webpack plugin)
IMOP, It is a non-starter if the final solution is vendor lock-in (e.g. requiring developers to use babel/webpack). AFAIK a lot of zero-runtime CSS-in-JS solution requires a compiler, and that compiler usually only being implemented in one ecosystem (as a babel plugin or webpack plugin)
That's what https://wyw-in-js.dev/ tries to solve and provides plugins for every bundler.
I wouldn't call using a compiler "vendor lock-in".
Most JS code already uses Babel or SWC. There is no dependency on a particular bundler in any of the compiler-based CSS solutions.
I agree that it would be great if in the future we could use both StyleX and MUI together and based on what got mentioned in the first message on top (of this RFC proposal) of this discussion, I think this is exactly what the MUI team is planning:
It should be possible to swap out the underlying CSS-in-JS pre-processor
So as I understand it, if StyleX has the required features (in the future) you will be able to swap the MUI CSS-in-JS pre-processor with StyleX (or any other css solution that meets the requirements)
From a strategic point of view I think it will benefit MUI if it is compatible with StyleX (at some point in the future), because I think StyleX will get a lot of traction for the simple reason that it is a library that is being made by the same company that is behind React and because they use it for projects like instagram
I however also understand why the MUI team is working on their custom solution right now, because they need a solution as soon as possible (and not one where the discussions haven't even started yet), the next.js app directory has been out for a while and I assume there are a lot of devs that are trying to use MUI with it, but then they read all the articles mentioning problems related to emotion and RSC and start to consider migrating to other UIs that already have zero runtime CSS-in-JS solution built in... it is great to hear that the StyleX devs are open for discussions as it means that in the future MUI + StyleX might be a thing but this is probably not soon enough for MUI
I don't think MUI working on their own custom CSS-in-JS pre-processor is a bad thing, it will be the "standard implementation" but that doesn't mean it will be the only solution, so to me it seems we get both, a solution that is ready sooner than later and the possibility to combine MUI with our favorite CSS solution (at a later date)
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 and Compiled.
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? ā
sx
prop along with container-specific props like<Box marginTop={1} />
etc.emotion
as well asstitches
, as mentioned below.What are our options? š”
We went through some of the existing zero-runtime solutions to see if they satisfy the above requirements.
.css.ts
file felt like a negative point in DX.sx
prop led us to pass on Linaria.styled
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 its extractor package.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. 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:
styled
API from Stitches. See this discussion 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 of
compiled
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 the
styled
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 the
styled
API or thesx
prop: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:
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 stylesBefore
After
Proposed API
The
styled
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
:The
theme
object above is passed through the bundler config. At build-time, this component would be transformed to something like that below (tentative):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
andstyle
props:Other top-level APIs would be:
css
to generate CSS classes outside of a component,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
.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: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 -
Related issue(s)