CSS in JS: The Argument Refined

From CSS to universal typed React UI bootstrap

UPDATE: I revamped design slightly and implementation a lot. Check Este next branch.

For years, developers have been fighting with complexity and related bugs of cascading style sheets. I have seen many styles, written in dozens various methodologies or without, worse or better, but almost always hard to manage and scale without rigorous supervision. Despite Less, SASS, and the other CSS preprocessors aimed to help, something was still broken in the land of styles.

For me, the eye-opener was famous “React: CSS in JS” talk. Vjeux thoroughly described seven main problems resulting from two main issues:

  1. styles are cascading
  2. styles are not written in JavaScript
https://speakerdeck.com/vjeux/react-css-in-js

Early adopters started experimenting with JavaScript objects projected as inline styles, but this approach wasn’t frictionless because inline styles don’t support pseudos and media queries. It took roughly two years and dozens of libraries to find the performant and efficient pattern, and I suppose (feel free to correct me) the Styletron was the first. Other libraries, for instance, Fela, quickly adopted the same approach.

As a creator of github.com/este/este, I was watching the scene carefully. Este is dev stack and starter kit for universal React. It’s something I do for my clients. It’s aimed to be the state of the art of current React app development. Don’t get me wrong, create-react-app and next.js are awesome, but their missions are different. Este is highly opinionated full-fledged starter kit for startups. It’s the model of web application they are going to write.

Este is universal (not only isomorphic), which means I strive for code reusability across all platforms (browser, server, mobile native, whatever). I knew CSS in JS is inevitable, simply because React Native has no global styles, but also JavaScript provides us greater control than plain CSS. I expected I would choose some nice looking CSS in JS library, will demonstrate it in Este, and everything will be fine. I was wrong. I wanted a banana but what I got were a gorilla holding the banana and the entire jungle.

I think the lack of reusability comes in object-oriented languages, not functional languages. Because the problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle. If you have referentially transparent code, if you have pure functions — all the data comes in its input arguments and everything goes out and leave no state behind — it’s incredibly reusable. (Joe Armstrong, creator of Erlang, on software reusability.)

rebass

The first library I experimented with was Rebass, because I liked its nice minimalistic API and design. Rebass taught me:

  1. We should build UI from named components, not from the combination of HTML5 (action or section?) tags and the set of classes. Element class is an implementation detail.
  2. We should prefer predefined constants over random values.

Unfortunately, Rebass is using inline styles and looks unmaintained.

styled-components

The second library I tried was styled-components.com. Familiar CSS strings with proper Atom highlight, support for React Native, and built on the top of Glamor. It was tempting. So I created Container component, then Text component, then I tried to create router Link and anchor component with Text styles and then fun ended. Having reusable string snippets could somehow work, but the code was pretty clunky. I asked in Github issues, but no one had a clear answer. Maybe my use case was too advanced and maybe they already fixed that, but I decided I need the real JavaScript anyway, not string snippets for one reason, the idea of typed styles.

typed styles

Este is flow typed, and styles should not be? What if our styles were statically typed as well? But that’s only possible with real JavaScript, not with strings. I believe the year 2017 will be the year of flowtype.org. Statical typing keeps code in check. It provides autocomplete, it helps us refactoring the code.

fela

So I reviewed all css-in-js libraries and then I found Fela. I was impressed. Fela had almost all features I need, almost no bugs, and Robin was very helpful to discuss and fix what I considered as not ideal. Fela uses plain JavaScript for styles, so I was able to experiment with typed styles. At first, I tried to mimic styled-components API, but without strings. I rewrote este.firebaseapp.com with that, but I wasn’t satisfied with the result. Style components were verbose, not explicit enough. But that step was inevitable. When we write new software, the first iteration is only a deep analysis of the problem. “Iterate, iterate, iterate.” (Not said by Lenin).

typography.js

Typograhy.js is not a typical CSS in JS library, but it was something I was very curious about as well. I believe web design is mainly about typography and colours.

Typography is a complex system of interrelated styles. 100s of style declarations on dozens of elements must be in harmonious order. Trying one design change can mean making dozens of tedious recalculations and CSS value changes. Creating new Typography themes with CSS feels hard. Typography.js provides a vastly simpler way to define and explore typography designs.

I desired to have such typography in Este as well.

Long story short, after several experiments and iterations, I knew what API and architecture I want.

CSS in JS as an implementation detail

Fela, glamor, styled-components, styletron, or any other CSS in JS library must be an implementation detail. Application source code must not reference any custom CSS in JS library, everything must be a component. With such contract, our application will not directly depend on any third-party code. We can change Fela for something else anytime without rewriting the rest of the application. The isolation is enforced via Box component. All other UI components are composed from Box.

Box is the basic building block for universal typed UI

Fela provides low level explicit API so we can make a beautiful abstraction on top of it. Box is the basic building block for any UI. It’s rendered as div or View (in React Native). Universal layout styles are exposed as props, any platform specific styles can be set via style prop. Style prop accepts a function with injected typed theme. Note paddingHorizontal={1}, it’s multiple of the baseline. More on that later. Look at Container composing Box.

Autocomplete and type checking out of the box.

But what if we already have some component and we want only style it? That’s what “as” prop is for.

And that’s whole Box API: Style props for basic styling, “style” prop with injected theme for any styling, “as” prop for styling any component.

If you are experienced with CSS in JS, maybe you are looking for props whitelist or blacklist. We don’t need that. Box component picks style props it uses and pass the rest props to underlaying component.

Having style props has another one nice consequence. We don’t have to create styled component for the sake of styled components. Naming is hard. If all we need is padding here and margin there, do we have to create a special component for that? No. Take a look at Footer:

In aforementioned example we see paddingVertical={1}. Props with size as value use multiples of the baseline. It helps us to ensure consistent sizing across the whole layout. With such approach, the maintaining vertical rhythm is easy, and we don’t have to think about it. Box borders are subtracted from padding. Look:

Baseline to demonstrate Vertical Rhythm

Vertical rhythm in design is as important as rhythm in music. Human brains like pattern repetition. Here is another example.

Traditionally, is has been hard to maintain rhythm, but with the power of JavaScript, it’s quite easy. I also implemented show baseline helper both for a browser and React Native.

Theme

The baseline is defined as absolute lineHeight in the theme file. Note the theme is typed as well (theme: Theme), so when we decide to redesign web or app, it helps us to keep correct styles.

We have a typography defined at the top of the theme definition, semantic colours, states, and constants for components. For example, here is Heading:

I think it’s self-describing. Heading is Text with default values possibly taken from theme. Note we set default values via ES6 destructuring assignment. It makes all Heading prop styles overridable so we can change anything anytime anywhere.

For more examples, check este.firebaseapp.com and github.com/este/este. I made the basic set of universal typed components with a minimalistic look so all other UI components can be made from them.

Everything is highly customisable. Not every app has to look like something made by Google. I prefer Medium minimal style, but nothing is wrong with eye candy look. It always depends on use case.

I haven’t covered everything, just illustrated the approach I decided to use in Este. Hope it helps.

Show your support

Clapping shows how much you appreciated Daniel Steigerwald’s story.