Modularise CSS the React way

To me, the following are the parts describing an UI component:

  • Content
  • (User) Interaction
  • Data
  • and, last but not least, Styling / Appearance

In the React universe, content and user interactions are declared using JSX and React uses the VirtualDOM abstraction to make updating these parts fast. Declaring and managing the data becomes easy when using Facebook’s GraphQL and Relay. But for the last part about styling, I am not so sure the current solution proposed by React, to use inline styles, is the best way — I think this area deserves more attention and work.

In the following I outline a proposal that concentrates on styling React components, which also allows developers to handle the styling information in a similar modular way as React does it today when dealing with data. This is achieved by introducing a new markup called MSS, which stands for Modular Style Sheet and is similar to CSS like JSX is similar to HTML elements and relying on what I call VirtualCSS, a similar idea to the VirtualDOM, that takes responsibility of optimising the declared styles.

TLDR: The last section summarises the discussed content.

Status Quo for styling React Components

Before we get started I want to cover quickly how styling works these days in React components. The current solution builds on the ideas from Christopher Chedeau (as he is known on the internet as “vjeux”, I will use this name in the following), discussed the problems with CSS for building larger applications in his presentation CSS in JS. Christopher also outlined a solution that is based on using inline styles. E.g. if we want to define a simple React component, that shows a notification, like

the corresponding JavaScript code looks like this:

var React = require('react');
var _ = require('lodash');
class MyNotification extends React.Component {
render() {
// Clone the CSS styles and set the border-color CSS property.
var styles = _.cloneDeep(this.constructor.styles);
styles.notification.borderColor = this.props.color;
    // Render the notification box.
return (
<div style={styles.notification}>
{this.props.prompt}
<span style={styles.notificationHint}>{this.props.hint} />
</div>);
}
}
// Define the inline CSS styles for the notification here.
MyNotification.styles = {
notification: {
border: "5px solid green",
padding: 10, // Becomes "10px" when rendered.
color: "#333"
},
notificationHint: {
fontStyle: "italic"
}
};

React.render(
<MyNotification prompt="I &hearts; modular CSS.",
hint="And so should you!" color="green"/>,
document.querySelector('#app-root'));

You see that the style definitions on MyNotification.styles are passed to the JSX elements via the as inline styles. While this works for the displayed exampled, I found inline styles to cause the following three problems for me:

  • CSS pseudo classes and media queries are not supported: Using inline styles doesn’t give you the full expressiveness compared to writing normal CSS: In particular, there is no way to express CSS selectors for pseudo classes like .notification:hover or .notification:before. Also, it is not possible to use media queries to adapt the styling based on view medium (e.g. adjust the content to look different when viewed on mobile vs. on desktop). Libraries like Radium offer a way to work around these limitations but have to do a nasty tricks to get there. Using tricks shouldn’t be necessary as browsers support these features out of the box already. This is a known limitation when using inline styles.
  • Break existing developer tools: For fine tuning CSS styles I ❤ using the developer tools provided by my browser. Tweaking the inline styles of an element is possible, but as the styles are defined separately on each mounted React element, changing the styles only affect a single React element and not all the React elements of the React component that I want to fine tune. Also, the changes disappear as soon as the React element gets unmounted, maybe throwing away the nicely tuned CSS styles before I was able to save them. (react-hot-loader makes this experience less painful but doesn’t solve the core issue.)
  • Creating CSS files for deployment is not that easy: When it comes to deploying a website extracting the style definitions into CSS files has a big advantage as the CSS files can be loaded in parallel with other resources. This results in a faster page load. Converting the inline style definitions to a CSS file for deployment can be done but I doubt this is a robust solution and is error prone.

So, let’s see how to fix these three issues and still prevent the issues listed in vjeux’s presentation.

Going one step back…

A very easy (and natural) solution to the disadvantages with inline styles arrises when doing the maybe most obvious thing and use an actual <style> element within the React component to declare the styling information. As the <style> element can hold all possible CSS this especially allows us to use pseudo selectors. Adjusting the MyNotification definition from before becomes then (including also a @media selector because we can do so with <style>):

class MyNotification extends React.Component {
render() {
return (
<div className="notification">
<style>
@media (min-width: 600px) {
.notification {
padding: 15px;
}
}
.notification {
border: 1px solid ${this.props.color};
padding: 10px;
color: #333;
}
/* Use nested CSS selectors - because we can! */
.notification span {
font-style: italic;
}
</style>
{this.props.prompt}
<span>{this.props.hint}</span>
</div>);
}
}

(The above code does not work due to other technical issues that I discuss here, but let’s not concentrate on it here as it is not important.)

This approach cannot be the solution — in fact, it has many of the problems that vjeux highlighted in his presentation, most importantly:

  • The selectors are not modular: To define the style for the nested <span> element the CSS selector .notification span is declared. This selector applies to all <span> elements under the notification’s <div> that has the .notification class applied. In particular this also means the CSS selector for our notification’s <span> applies to child components, that might use a <span> element as well.
  • Global CSS namespace: The component defines styling for the CSS class named .notification, but another developer might also use this CSS class name in a different component. As CSS class names are shared globally on a webpage, this causes naming conflicts and one developers will overwrite the rules of the other developer, leading to hard to track CSS bugs and therefore maintainability burden. Per see the global CSS namespace is not a problem, as long as the class names map to a globally unique class name and the React component knows the mapping from the local class name .notification to the unique name used on the page.

In the next section we will see how to resolve these two issues.

… and two steps forward

Following the best practises of React, information should be passed in the component hierarchy explicitly using the props attribute to gain modular components. Translated to the style definitions, there shouldn’t be a CSS selector that applies cross component boundaries. This can be achieve by restricting the CSS selectors and allow only plain class names and @rules as selectors. As CSS stands for Cascading Style Sheets I don’t think this name is suited here anymore (especially the Cascading bit) and therefore I call the new restricted syntax MSS, which stands for Modular Style Sheets. Instead of defining the CSS string within a <style> element, let’s assume for now there is a new React.Component#mountStyles method, which in our example is then used like this:

class MyNotification extends React.CSSComponent {
render() {
var styleMap = this.mountStyles`
@media (min-width: 600px) {
.notification {
padding: 15px;
}
}
.notification {
border: 10px solid ${this.props.color};
padding: 10px;
color: #333;
}
/* ".notification span" is rejected by ModularCSS as it
* contains a nested CSS selectors. Therefore, need to
* use a new class name instead. */
.notification-hint {
font-style: italic;
}`;
    return (
<div className={styleMap.notification}>
{this.props.prompt}
<span className={styleMap.notificationHint}>
{this.props.hint}
</span>
</div>);
}
}

This is very similar to the last discussed example: The this.mountStyles function checks there are no nested CSS selector used and throws an error if this is the case. As you can see from the comments, the attempt to use a selector like .notification span is prevented.

But after discussing how the styles are declared, how do they become affective on the web page and what’s the result stored on the styleMap object? Where JSX elements are applied to the browser using the VirtualDOM the MSS definitions get applied in the browser using what I call VirtualCSS. The VirtualCSS system takes care to transform the selector class names like .notification into a globally unique class name and then applies the transformed MSS to the browser inside of a <style> element — or for production write the rules out in a CSS file. As we will see in the next section the VirtualCSS is also crucial in optimising the CSS. In this particular example the VirtualCSS inserts a <style> element as the following one:

<style>
@media (min-width: 600px) {
.notification_12 {
padding: 15px;
}
}
.notification_12 {
border: 1px solid green;
padding: 10px;
color: #333;
}
.notification-hint_12 {
font-style: italic;
}
</style>

If a developer uses the .notification class name in a different component or a different this.props.color value is used, this is no problem, as the VirtualCSS will emit unique class names that are postfixed with a global counter number.

As the VirtualCSS maps the local class names like .notification to globally unique ones like .notification_12, the React component needs to know about this mapping when rendering the component. This is achieved by storing the mapping from the local to global names in the object returned from the this.mountStyles call — e.g. the styleMap variable contains the following definitions:

var styleMap = {
notification: 'notification_12',
notificationHint: 'notification-hint_12'
}

Using the MSS and VirtualCSS we get another nice functionality for free: Because the used CSS classes are defined locally within the React component, it is possible to perform static analysis if the style definitions are actually used on the rendered element. If this is not the case, a warning can be emitted. This ensures there are no unused CSS definitions in the component. This could also be checked using dynamic runtime checks, which stores the CSS class names defined by the call to this.moutStyles during the component’s render function and then check if the defined CSS classes are all used on the returned JSX element. This means, we get unused and undeclared CSS detection as a cheap byproduct.

Sprinting for performance

Introducing MSS and the VirtualCSS look already very promising, but there is one issue left: If two MyNotification elements are created with different colors, how to prevent a lot of duplicate CSS content to be created? This is especially important when it comes to compressing the CSS for deployment.

It turns out we are lucky here: Because the MSS is modular reordering the styles definitions is no problem and because the VirtualCSS has a global view on all the defined styles rules, combining style rules into common base definitions becomes possible. E.g. the following unoptimised CSS output

 <style> 
/* Result from mountCSS with this.props.colour="green" */
@media (min-width: 600px) {
.notification_12 {
padding: 15px;
}
}
.notification_12 {
border: 1px solid green;
padding: 10px;
color: #333;
}
.notification-hint_12 {
font-style: italic;
}

/* Result from mountCSS with this.props.colour="red" */
@media (min-width: 600px) {
.notification_42 {
padding: 15px;
}
}
.notification_42 {
border: 1px solid red;
padding: 10px;
color: #333;
}
.notification-hint_42 {
font-style: italic;
}
</style>

gets then converted into the (way more compressed) following output:

<style>
/*
Common styles for the colour="green" and colour="red"
* MyNotification container */
@media (min-width: 600px) {
.notification_12_42_common {
padding: 15px;
}
}
.notification_12_42_common {
border: 1px solid auto;
padding: 10px;
color: #333;
}
.notification-hint_12_42_common {
font-style: italic;
}
  /* Specialised styles for the MyNotification container */
.notification_12 { border-color: green; }
.notification_42 { border-color: red; }
</style>

You see that the system is quite smart: It realised that the border-color property is the only difference between the two <MyNotification … color=”green”> and <MyNotification … color=”red”> style declarations and therefore separated only this one into one class name .notification_12 and .notification_42 for each MyNotification configuration — the rest of the style definitions moved into the .notification_12_42_common class name. Because there are now two classes for style definition, the returned object from the this.moutStyles call contains now two classes as well:

// For the case of <MyNotification ... color="green">:
var
styleMap = {
notification: 'notification_12_42_common notification_12',
notificationHint: '.notification-hint_12_42_common'
}

For deployment the long class names can be replaced by smaller ones, which is not much of a deal thanks to the VirtualCSS abstraction.

Offroad:
What’s a good algorithm to group the CSS classes?

So far I didn’t talked about how the classes like .notification_12_42_common are computed. To be honest: I am pretty sure it can be done but I haven’t looked into it too much yet. Here are my thoughts so far on this issue:

In Unsupervised Machine Learning there are a bunch of techniques for grouping data. Unfortunate we cannot apply many of these techniques to our setup, as the space of CSS properties do not naturally induce an Euclidean Space. Luckily there are so called Non-metric relation clustering techniques, that are able to group nodes based on the relations between the nodes. Here the nodes are the CSS property-value-pairs and weighted edges between the nodes represent how often two CSS property-value-pairs are used together in the same CSS selector. The problem here is special though, as it requires to ensure certain nodes are never group together — e.g. if .notification_12 and .notification_42 would end up in the same group, the resulting CSS would no longer be correct. Algorithms developed in the context of clustering permission controls like in this paper might be a good idea to look at. Maybe it is also enough to go with a simple greedy algorithm for most cases.

And adding inline styles to the mix again

There is one last problem that we should solve though: If we look at the optimised CSS output we end up with a separate CSS class name selector per unique color property value on the <MyNotification … color=”aCSSColor”/> component:

  // For <MyNotification ... color="green" />
.notification_12 { border-color: green; }
  // For <MyNotification ... color="red" /> 
.notification_42 { border-color: red; }
  // For <MyNotification ... color="xyz" />
[A lot of other classes to cover all possible colors go here.]

Clearly this doesn’t scale well as in the worst case a separate class names for all possible color css values must be maintained. CSS classes pay off if style rules are applied to many elements. But in our case, if we want to set a single style on an element based on the this.props.color variable, it is better to go back to where we started and use inline styles again. Because class names and inline styles are now used together, the result from our call to this.mountStyles must include both and the return value might look like this:

var styleMap = {
// NOTE: The .notification_12 class name is gone. Instead the
// border color as defined in .notification_12 is now inlined
// on the style object.
notification: {
style: { borderColor: 'green' }, // For inline styles
className: 'notification_12_42_common' // For class names
},
notificationHint: {
style: { },
className: 'notification-hint_12_42_common'
}
}

To make the life of developers easier when applying the styleMap to the JSX elements, let’s assume a new css attribute is added, which is a short hand for applying the value to the style and className attribute directly. With all these changes, the final version of the MyNotification#render() method looks like this:

class MyNotification extends React.CSSComponent {
render() {
var styleMap = this.mountStyles`... same as before ...`;

return (
<div css={styleMap.notification}>
{this.props.prompt}
<span css={styleMap.notificationHint}>
{this.props.hint}
</span>
</div>);
}
}

Consider what we have achieved here: The developer doesn’t have to think about how the styles definitions propagate once passed to the this.mountStyles function to the eventually added DOM elements — all of the hard work is left to the VirtualCSS, which takes care of it. The VirtualCSS plays also an important role when working with React Native: as there are no actual <style> DOM elements in React Native, the only way to pass style information to the components is via the style attribute on the JSX elements. When using the VirtualCSS this isn’t a problem at all as in the context of React Native the VirtualCSS can fall back to declare all the style information using only inline styles and use no css classes at all.

Final word about modularity

A good practise when using React is to pass data along the component hierarchy in and this way avoid the individual components to do the data fetching themselves. This turns out to make the React components become very modular. The React components that take over the data handling are so called Container Components, which get data (e.g. from Flux stores) and then pass the data down to all child components under this Container Component using the prop attribute of the child components. (See the blog post linked to Container Components for more details). In a similar fashion, can React Components become more modular by moving the styling definition out?

I think this is a good idea to consider and therefore extend the Container Components to not only support data but also style declarations. In a sense, we can think about a style guide as a Flux Store, that provides styling information and the job of the Container Component is to perform the mapping between the style guide and the actual style rules to apply in the nested comonent hierachy below the Container Component.

Summary

If you have made it to the end here, you have come a long journey. What have we achieved by now?

  • By leveraging CSS definitions over inline styles we are able to recover features like pseudo selectors for :hover and @media queries.
  • Instead of using inline styles to define the React components visuals, we introduced a new markup called MSS, which is similar to CSS but with a few additional restrictions to make the CSS definitions become modular.
  • By cupeling the style definitions closely with the React components it is possible to check for unused / undefined CSS declarations.
  • Managing the style definitions is done using VirtualCSS, which can optimise the style definitions given the modular property of MSS declarations and the global view on all MSS declarations.
  • By leaving the choice how to represent the styles definitions to VirtualCSS and uniting the style and className properties into a new css property gives extra flexibility when optimising the final CSS.
  • Where React concentrated before on passing only data into the component via props it becomes now clearer to do a similar thing with style definitions: In this setting a Container Component does not only provide data to the child components but also the styling information.

At this point, there is no full implementation on the ideas outlined here yet, though a minimal implementation of MSS and VirtualCSS without any further CSS optimisation exists and works fine for me so far. Writing this blog post sucked quite a bit of energy out of me but I hope to work on this system more soon and OpenSource the implementation.

Please let me know what you think about this idea — either by leaving a comment/response here on Medium or reach out to me on Twitter/G+.

Thanks for reading!

PS: Special thanks go to James Long for reading a first draft and providing valuable feedback!