style9: build-time CSS-in-JS

Avatar of Johan Holmerin
Johan Holmerin on

In April of last year, Facebook revealed its big new redesign. An ambitious project, it was a rebuild of a large site with a massive amount of users. To accomplish this, they used several technologies they have created and open-sourced, such as React, GraphQL, Relay, and a new CSS-in-JS library called stylex.

This new library is internal to Facebook, but they have shared enough information about it to make an open-source implementation, style9, possible.

Why another CIJ library?

There are already plenty of CSS-in-JS (CIJ) libraries, so it might not be obvious why another one is needed. style9 has the same benefits as all other CIJ solutions, as articulated by Christopher Chedeau, including scoped selectors, dead code elimination, deterministic resolution, and the ability to share values between CSS and JavaScript.

There are, however, a couple of things that make style9 unique.

Minimal runtime

Although the styles are defined in JavaScript, they are extracted by the compiler into a regular CSS file. That means that no styles are shipped in your final JavaScript file. The only things that remain are the final class names, which the minimal runtime will conditionally apply, just like you would normally do. This results in smaller code bundles, a reduction in memory usage, and faster rendering.

Since the values are extracted at compile time, truly dynamic values can’t be used. These are thankfully not very common and since they are unique, don’t suffer from being defined inline. What’s more common is conditionally applying styles, which of course is supported. So are local constants and mathematical expressions, thanks to babel’s path.evaluate.

Atomic output

Because of how style9 works, every property declaration can be made into its own class with a single property. So, for example, if we use opacity: 0 in several places in our code, it will only exist in the generated CSS once. The benefit of this is that the CSS file grows with the number of unique declarations, not with the total amount of declarations. Since most properties are used many times, this can lead to dramatically smaller CSS files. For example, Facebook’s old homepage used 413 KB of gzipped CSS. The redesign uses 74 KB for all pages. Again, smaller file size leads to better performance.

Slide from Building the New Facebook with React and Relay by Frank Yan, at 13:23 showing the logarithmic scale of atomic CSS.

Some may complain about this, that the generated class names are not semantic, that they are opaque and are ignoring the cascade. This is true. We are treating CSS as a compilation target. But for good reason. By questioning previously assumed best practices, we can improve both the user and developer experience.

In addition, style9 has many other great features, including: typed styles using TypeScript, unused style elimination, the ability to use JavaScript variables, and support for media queries, pseudo-selectors and keyframes.

Here’s how to use it

First, install it like usual:

npm install style9

style9 has plugins for Rollup, Webpack, Gatsby, and Next.js, which are all based on a Babel plugin. Instructions on how to use them are available in the repository. Here, we’ll use the webpack plugin.

const Style9Plugin = require('style9/webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  module: {
    rules: [
      // This will transform the style9 calls
      {
        test: /\.(tsx|ts|js|mjs|jsx)$/,
        use: Style9Plugin.loader
      },
      // This is part of the normal Webpack CSS extraction
      {
        test: /\.css$/i,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      }
    ]
  },
  plugins: [
    // This will sort and remove duplicate declarations in the final CSS file
    new Style9Plugin(),
    // This is part of the normal Webpack CSS extraction
    new MiniCssExtractPlugin()
  ]
};

Defining styles

The syntax for creating styles closely resembles other libraries. We start by calling style9.create with objects of styles:

import style9 from 'style9';

const styles = style9.create({
  button: {
    padding: 0,
    color: 'rebeccapurple'
  },
  padding: {
    padding: 12
  },
  icon: {
    width: 24,
    height: 24
  }
});

Because all declarations will result in atomic classes, shorthands such as flex: 1 and background: blue won’t work, as they set multiple properties. Properties that can be can be expanded, such as padding, margin, overflow, etc. will be automatically converted to their longhand variants. If you use TypeScript, you will get an error when using unsupported properties.

Resolving styles

To generate a class name, we can now call the function returned by style9.create. It accepts as arguments the keys of the styles we want to use:

const className = styles('button');

The function works in such a way that styles on the right take precedence and will be merged with the styles on the left, like Object.assign. The following would result in an element with a padding of 12px and with rebeccapurple text.

const className = styles('button', 'padding');

We can conditionally apply styles using any of the following formats:

// logical AND
styles('button', hasPadding && 'padding');
// ternary
styles('button', isGreen ? 'green' : 'red');
// object of booleans
styles({
  button: true,
  green: isGreen,
  padding: hasPadding
});

These function calls will be removed during compilation and replaced with direct string concatenation. The first line in the code above will be replaced with something like 'c1r9f2e5 ' + hasPadding ? 'cu2kwdz ' : ''. No runtime is left behind.

Combining styles

We can extend a style object by accessing it with a property name and passing it to style9.

const styles = style9.create({ blue: { color: 'blue; } });
const otherStyles = style9.create({ red: { color: 'red; } });

// will be red
const className = style9(styles.blue, otherStyles.red);

Just like with the function call, the styles on the right take precedence. In this case, however, the class name can’t be statically resolved. Instead the property values will be replaced by classes and will be joined at runtime. The properties gets added to the CSS file just like before.

Summary

The benefits of CSS-in-JS are very much real. That said, we are imposing a performance cost when embedding styles in our code. By extracting the values during build-time we can have the best of both worlds. We benefit from co-locating our styles with our markup and the ability to use existing JavaScript infrastructure, while also being able to generate optimal stylesheets.

If style9 sounds interesting to you, have a look a the repo and try it out. And if you have any questions, feel free to open an issue or get in touch.

Acknowledgements

Thanks to Giuseppe Gurgone for his work on style-sheet and dss, Nicolas Gallagher for react-native-web, Satyajit Sahoo and everyone at Callstack for linaria, Christopher Chedeau, Sebastian McKenzie, Frank Yan, Ashley Watkins, Naman Goel, and everyone else who worked on stylex at Facebook for being kind enough to share their lessons publicly. And anyone else I have missed.