DriveTribe is transitioning from a monolithic CSS file generated with CSS Modules, to a CSS-in-JS approach using Styled Components (SC).

We’ve been at this for a couple months. Initially, progress was tentative and evaluative. Now, a PR isn’t passed if it touches an old component without porting it to SC.

So, I’m going to share some of our findings, and explain why our confidence in SC boomed in such a short space of time.

This isn’t a volley in the eye-rolling CSS-in-JS vs preprocessor war. It’s also not a stream of platitudes about why SC is the second coming. It’s just an explaination of why SC is a good, but not perfect, match for the culture at DriveTribe.

Who we are

First, it’s helpful to describe the nature of working in a front end team in a startup like DriveTribe, to help frame why SC is a good fit for us. You might recognise some, or all of these traits within your own team:

We create features, often. We spin up new features at a breakneck pace to test ideas out with our users. This has the potential to inflate the codebase to the point that, even within a small team, is hard to maintain a full mental model of.

We kill features, often. We’re quick to kill off features that don’t work, but cleanups aren’t always perfect. And sometime’s we’re not that quick, either.

We iterate, rapidly. The features that do survive, the “idea tests”, are the quintessential eternal prototype. Sometimes there simply isn’t time to go back and brush up some gnarly hack, so the simpler, cleaner, and more modular that we can make that initial code, the better.

We prefer functional, declarative code. It almost comes with the territory when you go all-in with React and Redux, but this style of code is very easy to read and reason about, so we err towards that style across the codebase.

So what’s so effin great about Styled Components?

With these traits in mind, let’s take a look at the benefits (and not-benefits) of SC that we’ve identified so far.

1. Gradual implementation

It was really important for us to be able to experiment with SC before making any commitment. The size of the codebase meant that a big bang approach would simply never be possible, from a time investment, design integrity, or pure human sanity point of view.

It’s easy to make just a single component with SC, or a series of components in a part of your site in order to stress-test its performance. You’ll quickly come to know if it’s right for your project.

But… SC isn’t free. It brings with it a ~12kb overhead, which is frankly quite a bloody lot to load a sprinkling of CSS. Ultimately, if you’re loading 12kb of JavaScript for any reason, it needs to be integral. For that reason, you don’t want to be in this transitionary period for long.

2. Semantic tags and clear logic

Using semantic HTML tags like article and li correctly is critical for screen readers and bots to correctly parse our websites.

Typically attempts to make HTML semantic for humans centre on choosing descriptive, non-presentational class names:

<ul class="post-list">

<li class="post highlighted"></li>

<li class="post"></li>

</ul>

The code that generates this markup can look messy with any templating framework. To take the React & CSS Modules approach, using Classnames to handle the logic of which classes to apply, you can end up with code that looks like this:

<ul className={styles.postList}>

{posts.map(({ isHighlighted }) => {

const className = classnames(styles.post, {

[styles.highlighted]: isHighlighted

}); return (

<li className={className}></li>

);

})}

</ul>

There are clearly ways of making this look better, for instance abstracting the list item to its own component. But, that classnames logic is still going to have to live somewhere, and adding extra presentational states becomes unwieldy.

There’s also nothing stopping styles.highlighted from being silently undefined , which has lead to presentational bugs. On the flip side, unused classes are compiled and sent to users.

Finally, this feels like a lot of implementation detail. I shouldn’t have to care that we arrive at our presentational state by conditionally concatenating classes, or even that we’re hooking into a class at all.

With Styled Components, our code looks like this:

<PostList>

{posts.map(({ isHighlighted }) => (

<PostItem isHighlighted={isHighlighted}></PostItem>

))}

</PostList>

Semantically, PostList means more to me as a human being and a developer than ul .

If PostList or PostItem is undefined , this code will throw an error. Meanwhile unused components are ignored entirely thanks to Webpack’s tree-shaking capabilities.

And finally, the logic of isHighlighted is exposed in the CSS like this:

export const PostItem = styled.li`

/*...styles*/

${(props) => props.isHighlighted && `animation-name: blink`}

`;

I don’t care that this logic leads to a new class being created. Incidentally, it does.

But… This approach, of hiding tag names, can make it tougher to create valid HTML. For instance, li must be a direct child of ul or ol . This is clearly true in the first example, but isn’t as clear in the second.

This risk can be mitigated with naming conventions (like appending List and Item as above), but to some degree, it persists.

3. No duplicate variables

Invariably, there’s a time when you need a duplicate CSS variable in your JavaScript. It might be for a layout calculation, animation, or an interaction. It happens.

It happens rarely enough that you might even be tempted to store the variable locally in the component you’re using it in. And again, that inevitable second time.

It’s a fight you’ve already lost. You know the rest of this story. By storing all these variables in one place, JS, that nonsense stops.

But… During the transitionary period, you’re maintaining mirror variables in both CSS and JS. In our case, this has luckily coincided with a transitionary period for our design language so the new stuff is in JS and the old stuff is in the CSS. YMMV.

4. Module resolution

Absolute paths are helpful for maintaining a flexible codebase. After configuring Webpack to support these, the SASS @import directive still didn’t respect them. Counting levels up a folder tree isn’t something we have to do any more.

5. JavaScript

With CSS Modules, we were also using SASS (which is lovely). They have different and slightly overlapping capabilities, so a few composes here and some @include s there, we were left with a bit of a mishmash.

These different directives and special rules live in the same file but they’re not interoperable. They’re also proprietry, and having to live and be parseable amongst pedestrian CSS they generally end up with weird syntax.

These are normal template literals outputting normal CSS. “Funny business” is handled with JS functions, many of which you may already use and be familiar with elsewhere on your site.

The only remaining domain knowledge is the Styled Components API, which is, yes, JS. You won’t be Googling “@mixin” again.

But… SASS and LESS are preprocessors. All this JS is just typical processing, bytes that you’re leaving for your user’s network to download and CPU to execute.

Theoretically, this leads to some level of reduced performance. However, we’ve run fairly intense chat rooms (1000s of messages per minute) with SC in place and they’ve been fine even on older processors. It is unlikely, but possible, that this has an impact outside of benchmarks.

6. Removing the render-blocking request

Server-side rendering (SSR) is, in my mind, a killer feature of React (and any other frameworks that support it).

It’s a massive win for performance, especially for users who live with poor-quality connections or slow devices. Used correctly, it enables people to use the website without the client JS ever loading.

It’s also a massive win for developers. Once you manage to switch your mental model of how the website hangs together, it’s easier to reason about a single codebase that becomes richer when it mounts, than it is to think about writing a HTML-generator in one language that has this weird extra runtime in JS that hooks in with brittle query selectors.

Even with SSR, the site still won’t render until the required CSS is downloaded and parsed. This extra network request is a blocking request. We can remove this final external dependency with SC:

const html = renderToString(

stylesheet.collectStyles(<Site />)

);

const css = stylesheet.getStyleTags();

With these two strings we can output a webpage that can be enjoyed by users as soon as the initial request is parsed. No render-blocking external dependencies required.

Crucially, only CSS required for the current render is inlined. This makes it trivial to optimise the critical rendering path.

By rendering only the components required for the initial view, and then asynchronously loading the data and components requried for the rest of the page, the user will only have to download the HTML and CSS neccessary for the first view. This is a massive win for the perceived speed of your site.

This isn’t a step we’ve taken yet, but the pieces are in place to do so in the near future.

But… Like the spoon, for this one there is no but.

This one’s just awesome.

Conclusion

Styled Components isn’t perfect, but for our usecase we have found it an improvement over CSS Modules.

It produces cleaner code, throws errors, consolidates our codebase, lets us reorganise components freely and can lead to big performance wins.

Though, many of these come with associated tradeoffs. There are probably plenty of pros and cons I’m not even aware of (let me know!)

Most people I’ve spoken to who dismiss it out of hand generally show the same visceral reaction that most of us had to JSX. The mere mention of CSS-in-JS can elicit head snaps so violent that I am left wondering whether I just said “Styled Components” or, actually, “go fuck yourself.”

I guess I’ll never know.

If your work environment sounds familiar to ours, I would recommend that, at the very least, you give it a real go with an open mind. Write a component or three, let them hang out with the rest of your code. If you choose a specific part of your site, code splitting will minimize the effect on your bundle.

Everything beyond that will happen naturally, if it’s the right fit.