This RFC is a proposal for changing the styling solution of Material-UI in v5.
TL:DR; the core team proposes we go with emotion
What's the problem?
- Maintaining & developing a great styling engine takes a considerable amount of time. We have experienced it first hand. Over the last 12 months, we have preferred to invest time on our core value proposition: the UI components, rather than improve the style engine. Working on it has a high opportunity cost.
- We have been facing issues with supporting dynamic styles for the components. The performance of our custom dynamic styles implementation (based on props) isn't great (see the performance benchmarks below). This is seriously limiting the quality of the Developer Experience we can provide. It's a blocker for improving our API around customizability or ease of writing styles. For instance, it will unlock: style utils props, color variant, and custom variant.
- The React community, at large, hasn't voted for using JSS at scale (JSS is great and still used). 3 years ago we bet on the best option available. We have to recognize better options are available now. We can move faster and unlock better DX/UX by building on top of a more popular, existing, styling solution.
- Many developers use styled-components to override Material-UI's styles. End-users find themselves with two CSS-in-JS libraries in their bundle. Not great. It would be better if we could offer different adapters for different CSS-in-JS libraries. (Potential problems: we may need to re-write the core styles to match the syntax of the engine used 🤷♀️)
What are the requirements?
Whatever styling engine we choose to go with we have to consider the following factors:
- performance: the faster the better but we are willing to trade some performance to improve the DX.
- bundle size: below our current 14.3 kB gzipped would be great.
- support concurrent mode:
@material-ui/styles has partial support as I'm writing.
- support SSR
- simple customization
- allow dynamic styling
- good community size
- flat specificity
It would be nice if it can support the following:
- zero-config from the perspective of Material-UI consumers
- streaming https://github.com/mui-org/material-ui/issues/8503
- source map
What are our options?
- JSS (currently wrapped in material-ui)
Here are benchmarks with dynamic styles of several popular libraries (note the Material-UI v4 only use static styles which have good performance):
PR for reference: https://github.com/mnajdova/react-native-web/pull/1
Based on the performance, I think that we should eliminate: JSS (currently wrapped in @material-ui/styles), styletron, and fela. That would leave us with:
Based on the open issues, it seems that Aphrodite doesn't support dynamic props: https://github.com/Khan/aphrodite/issues/141
which in my opinion means that we should drop that one from our options too, leaving us with:
emotion are both libraries are pretty popular,
react-styletron at the time or writing is much behind with around 12500 downloads per week (this in my opinion is a strong reason why we should eliminate it, as if we decide to go with it, the community will again need to have two different styling engine in their apps).
Here is the list rang by the number of Weekly downloads at the time of writing:
Note that storybook has a dependency on emotion. It significantly skews the stats.
Support concurrent mode
- emotion: YES. Since v10 it is strict mode compatible based on their announcement post. I have tested it on a simple project that works as expected.
- styled-components: Partial. There is at least one bug with global styles in strict mode.
- emotion: YES. https://emotion.sh/docs/ssr. Also has an interesting no configuration support for prototyping only.
- styled-components: YES. https://styled-components.com/docs/advanced
- styled-components: 30.6k
- emotion: 11.4k
- ~~JSS~~: 5.9k
Trafic on the documentation
SimilarWeb estimated sessions/month:
- ~~sass-lang.com~~: ~476K/month (for comparison)
- styled-components.com: ~239K/month
- emotion.sh: ~59K/month
- ~~cssinjs.org~~: <30k/month (for comparison)
Based on the survey, 53.8% percent are using the Material-UI styles (JSS), which is not a surprise as it is the engine coming from Material-UI. However, we can see that 20.4% percent are already using styled-components, which is a big number considering that we don't have direct support for it. Emotion is used by around 1.9% percent of the developers currently based on the survey.
Having these numbers we want to push with better support for styled-components, so this is something we should consider.
- emotion: modern evergreen browsers + IE11
- styled-components: not documented for v5, but the previous versions support the following
What's the best option?
Even if we decide to support multiple engines, we would still need to advocate for one by default and have one documented in the demos.
- Has the biggest community, people love to use it.
- Performance starting from v5 is good.
- It will mean that all components styles need to be created using the
styled API, which means for developers they will always have wrapper components if they need to re-style.
- Lack of full concurrent support, which may create blockers down the road.
- Relatively large community, growing.
- Good performance.
- Concurrent mode + SSR would be possible out of the box.
- The CSS prop can be useful for overrides.
- Source map support.
- A bit smaller.
We may try to support multiple CSS-in-JS solutions, by providing our in house adapters for them. Some things that we need to consider is that, that we may have duplicate work on the styles, as the syntax is different between them (at least jss compared to styled-components/emotion). We will reuse the theme object no matter what solution we will pick up.
The less involved support for this may come from the usage of the
styled, as people may do some webpack config to decide which one to use - (this is just something to consider).
Deterministic classnames on the components that can be targeted for custom styles
Regarding how the classes look and how developers may target them, I want to show a comparison of what we currently have and how the problem can be solved with the new approach.
As an example, I will take the Slider component. Here is currently how the generated DOM look like:
Each of the classes has a very well descriptive semantic and people can use these classes for overriding the styles of the component.
On the other hand, emotion, styled-components or any other similar library will create some hash as a class name. For us to solve this and offer the developers the same functionality for targeting classes, each of the components will add classes that can be targeted by the developers based on the props.
This would mean that apart from the classes generated by emotion, each component will still have the classes that we had previously, like
MuiSlider-colorPrimary, the only difference would be that this classes will now be used purely as selectors, rather than defining the styles for the components. This could be implemented like a hook - useSliderClasses
No matter which solution we would choose, we would use the
styled API, which is supported by the two of them. This will allow us down the road to have easier support for styled + unstyled components (probably with webpack aliases, like for using preact).
After we investigated the two options we had in the end, the core team proposes we go with emotion. Some key elements:
A small migration cost between styled-components and emotion
Developers already using styled-components should be able to use emotion with almost no effort.
There are different ways for adding overrides other than wrapper components
The support of cx + css from emotion can be beneficial for developers to use it as an alternative for adding style overrides if they don't want to create wrapper components.
Concurrent mode is for sure supported :+1:
Kudos to @ryancogswell for doing a deeper investigation on this topic. So far we did not find anything in @emotion's code that would give us concern that concurrent mode wouldn't work.
We were also looking into
createGlobalStyle from styled-components as a comparison to emotion's Global component. It is doing most of its work during render (inherently problematic for Strict/Concurrent Mode) and just using useEffect for removing styles in its cleanup function. createGlobalStyle needs a complete rewrite before it will be usable in concurrent mode -- it isn't OK for it to add styles during render if that render is never committed. It looks like someone has tried rewriting it with some further changes in the last month, so we will need to follow this progress.
How is the specificity handled
Emotion's docs recommend doing composition of CSS into a single class rather than trying to leverage styles from multiple class names. In v5, our existing global class names would be applied without any styles attached to them. The composition of emotion-styled components automatically combines the styles into a single class. This potentially gets rid of these stylesheet order issues at least internal to the styles defined by Material-UI, because every component's styles are driven by a single class name :+1:.
So we would have the global class names (for developers to target in various ways for customizations) and then a single generated (by emotion) class name per element that would consolidate all the CSS sources flowing into it.
Specificity is then handled by emotion based on the order of composition.
All compositions using emotion (whether render-time or definition-time composition) results in a single class on the element.
styled-components does NOT work this way concerning render-time composition (definition-time composition does get combined into a single class). The same composition in styled-components results in multiple classes applied to the same element and the specificity does not work as I would have intended.
What do you think about it?