The styled-components Happy Path

My personal suite of “best practices”

For a few years now, my #1 favourite tool for managing CSS in React apps has been 💅 styled-components.

It's a wonderful tool. In many ways, it's changed how I think about CSS architecture, and has helped me keep my codebase clean and modular, just like React!

It shares something else in common with React: developers often dislike the idea at first 😅. “Every style is a component” can be a hard pill to swallow, just like “your views are now written in an XML/JS hybrid”.

Maybe as a result, I've discovered that a lot of developers never really fully embrace styled-components. They pop it into their project without updating their mental models around styling. One foot in, and one foot out. As a result, they miss out on some of the best parts of the tool!

If you work with styled-components, or a similar tool like Emotion, my hope is that this article will help you get the most out of it. I've distilled years of experimentation and practice into a few practical tips and techniques. If you apply these ideas, I genuinely believe you'll be a happier CSS developer ✨

Let's start with a fun little tip.

Say we have a Backdrop component, and it takes props for opacity and color:

jsx

How do you apply those properties to the Wrapper?

One way would be to use an interpolation function:

jsx

This works alright, but it's fairly high-friction. It also means that whenever these values change, styled-components will need to re-generate the class and re-inject it into the document's <head>, which can be a performance liability in certain cases (eg. doing JS animations).

Here's another way to solve the problem, using CSS variables:

jsx

CSS variables are the gift that keeps on giving. If you're not sure what's going on here, my CSS Variables in React tutorial will help you make sense of it (plus you'll learn a few other neat tricks!).

We can also use CSS variables to specify default values:

jsx

If we call <Backdrop> without specifying an opacity or color, we'll default to 75% opaque, and our color theme's dark gray color.

It just feels nice. It isn't game-changing, but it brings me a little bit of joy.

But that's just the beginning. Let's look at something meatier.

Link to this heading
Single source of styles

If you only take one thing away from this blog post, make it this tip. This is the mother lode.

On this blog, I have a TextLink component. It looks something like this:

jsx

This is the component I use for links within content, like this blog post. Here's an example (Note: This link is meant to be a visual demo, you aren't meant to click it).

On my blog, I have an Aside component which is used to provide bonus little bits of information:

In that Aside, the words “an included link” are rendered with a TextLink, the very same component! I wanted to apply some different styles, though; I didn't love having blue text on a blue background.

This is what I'd call a “contextual style”. The same component changes appearances depending on its context. When you pop a TextLink into an Aside, some styles are added/replaced.

How would you solve for this situation? I often see stuff like this:

jsx

In my opinion, this is a five-alarm-fire situation. We've made it so much harder to reason about the styles in our application!

How would you ever find out that TextLink could be given these styles? You can't do a project-wide search for TextLink. You'd have to grep for a, and good luck with that. If we don't know that Aside applies these styles, we'll never be able to predict it.

So OK, what's the right approach? Maybe you've thought about specifying these styles using TextLink instead of a:

jsx

styled-components allows us to “embed” one component in another like this. When the component is rendered, it pops in the appropriate selector, a class that matches the TextLink styled-component.

This is definitely better, but I'm not a happy camper yet. We haven't solved the biggest problem, we've just made it slightly easier to work around.

Let's take a step back and talk about encapsulation.

The thing that made me love React is that it gives you a way to pack logic (state, effects) and UI (JSX) into a reusable box. A lot of folks focus on the "reusable" aspect, but in my opinion, the cooler thing is that it's a box.

A React component sets a strict boundary along its perimeter. When you write some JSX in a component, you can trust that the HTML will only be modified from within that component; you don't have to worry about some other component on the other side of the app "reaching in" and tampering with the HTML.

Take another look at that TextLink solution. The Aside is reaching in and meddling with TextLink's styles! If any component can overwrite any other component's styles, we don't really have encapsulation at all.

Imagine how much nicer it would be if you knew, with complete confidence, that all of the styles for a given element were defined right there, in the styled-component itself?

Well, it turns out, we can do that. Here's how:

jsx
jsx

If you're not familiar with the & character, it's a placeholder for the generated class name. When styled-components creates a .TextLink-abc123 class for this component, it'll also replace any & characters with that selector. Here's the CSS it generates:

css

With this little trick, we've inverted the control. We're saying “Here are my base TextLink styles, and here are the TextLink styles when I'm wrapped in AsideWrapper”. All in 1 place.

Our mighty TextLink is in charge of its own destiny once more. We have a single source of styles.

Doing it this way is seriously so much nicer. Give it a shot the next time you run into this situation.

Alright, I have one more big idea to share.

Let's say that we want that Aside component to have some space around it, so that it isn't stuck right up against its sibling paragraphs and headings.

Here's one way to do that:

jsx

This'll solve our problem, but it also feels a bit pre-emptive to me. We've locked ourselves in; what happens when we decide to use this component in another situation, one with different spacing requirements?

There's also the fact that margin is weird. It collapses in surprising and counterintuitive ways, ways that can break encapsulation; if we put our <Aside> inside a <MainContent>, for example, that top margin will push the entire group down, as if MainContent had margin.

(Hover or focus this visualization to see what I mean!)

I recently wrote about the Rules of Margin Collapse. If you're surprised to learn that margins behave in this way, I think you'll find it super valuable!

There's a growing movement of developers choosing not to use margin at all. I haven't yet given up the habit entirely, but I think avoiding "leaky margin" like this is a great compromise, a great place to start.

How do we do spacing without margin? There are a few options!

Ultimately, the goal is to avoid painting ourselves into a corner. I believe it's fine to be pragmatic and use margin occasionally, so long as we're intentional about it, and we understand the trade-offs.

Finally, we need to chat about stacking contexts.

Take a critical look at this code:

jsx

See the problem? Similar to before, we've pre-emptively given our component a z-index. We better hope that 2 is the right layer in all future usecases!

There's an even-more-pernicious version of this problem as well. Take a look at this code:

jsx

The top-level styled-component, Wrapper, doesn't set a z-index… Surely, this must be fine??

I wish it were so. In fact, this situation can lead to a super confusing issue.

If our Flourish component has a sibling with an in-between z-index, it'll get "interleaved" between the bit and its background:

jsx

We can solve for this by explicitly creating a stacking context, using the isolation property:

jsx

This ensures that any sibling elements will either be above or below this element. As a bonus, the new stacking context doesn't have a z-index, so we can rely purely on DOM order, or pass a specific value when we know what it needs to be.

This stuff is complicated, and way outside the scope of this article. Stacking contexts are covered in depth in my upcoming CSS course, CSS for JavaScript Developers.

Link to this heading
Misc tips and tricks

Phew! We've covered the high-level “big ideas” I wanted to share, but before I wrap up, I have a few smaller tidbits I think are worthwhile. Let's go through them.

React developers have a reputation for being ignorant of semantic HTML, using <div> as a catch-all.

Two birds sitting in a tree. The first says “Semantic HTML conveys meaning”, but is interrupted by a larger bird screaming “<div>” and “<div class='button'>”.

A fair criticism of styled-components is that it adds a layer of indirection between the JSX and the HTML tags being produced. We need to be aware of that fact, so that we can account for it!

Every styled-component you create accepts an as prop which'll change which HTML element gets used. This can be really handy for headings, where the exact heading level will depend on the circumstance:

jsx

It can also come in handy for components that can either render as buttons or links, depending on the circumstance:

jsx

Semantic HTML is very important, and the as prop is a crucial bit of knowledge for all developers building with styled-components.

Link to this heading
Increasing specificity

In most CSS methodologies, you'll occasionally run into situations where a declaration you write has no effect because another style is overwriting it. This is known as a specificity issue, since the undesirable style is "more specific" and wins.

For the most part, if you follow the techniques laid out in this article, I promise that you won't have specificity issues, except possibly when dealing with third-party CSS. This blog has ~1000 styled-components, and I've never had specificity problems.

I am hesitant to share this trick, because it's an escape hatch for a situation that should really be avoided… But I also want to be realistic. We all work in codebases that are not always ideal, and it never hurts to have an extra tool in your toolbelt.

Here it is:

jsx

In this situation, we have three separate color declarations, targeting the same paragraph.

At the base level, our Paragraph is given red text using the standard styled-components syntax. Unfortunately, the Wrapper has used a descendent selector and has overwritten that red text with blue text.

To solve this problem, we can use a double-ampersand to flip it to green text.

As we saw earlier, the & character is a placeholder for the generated class name. Putting it twice repeats that class: Instead of .paragraph, it'll be .paragraph.paragraph.

By ”doubling down” on the class, its specificity increases. .paragraph.paragraph is more specific than .wrapper p.

This trick can be useful for increasing specificity without reaching for the nuclear option, !important. But there's a bit of a pandora's box here: once you start going down the specificity-tricks road, you're on the path towards mutually-assured destruction.

Link to this heading
The babel plugin

In production, styled-components will generate unique hashes for each styled-component you create, like .hNN0ug or .gAJJhs. These terse names are beneficial, since they won't take up much space in our server-rendered HTML, but they're completely opaque to us as developers.

Thankfully, a babel plugin exists! In development, it uses semantic class names, to help us trace an element/style back to its source:

screenshot showing the Chrome DevTools elements pane, and an element with a filename + styled-component name given

If you use create-react-app, you can benefit from this plugin without needing to eject by changing all your imports:

jsx

A quick find-and-replace in your project will dramatically improve your developer experience!

For other types of projects, you can follow the official documentation.

In this article, we've looked at some styled-components-specific APIs, but really the ideas I hope to convey are bigger than any specific tool or library.

When we extend the component mindset to our CSS, we gain all sorts of new superpowers:

  • The ability to know, with confidence, whether it's safe to remove a CSS declaration (no possibility of it affecting some totally-separate part of the application!).
  • A complete lack of specificity issues, no more trying to find tricks to bump up specificity.
  • A neat and trim mental model that fits in your head and helps you understand exactly what your pages will look like, without needing to do a bunch of manual testing.

styled-components is relatively unopinionated, and so there are a lot of different ways to use it… I have to admit, though, it makes me a bit sad when I see developers treat it as a fancy class-name generator, or “Sass 2.0”. If you lean in to the idea that styled-components are components, first and foremost, you'll get so much more out of the tool.

These are, of course, only my opinions, but I was happy to learn that they're in-line with recommended practices. I sent an early draft of this post to Max Stoiber, creator of styled-components, and here's how he responded:

Screenshot of a Twitter DM from Max, saying that the post is really good, and that it makes him happy to see that I 'get' styled-components

A lot of this stuff has faded into view for me, becoming crisp only after a few years of experimentation. I hope that this post saves you some time and energy.

Also! I'm currently working on a course called CSS for JavaScript Developers. It's a CSS fundamentals course for folks who have learned enough CSS to get by, but not enough to feel comfortable. It's specifically tailored for JS developers, devs who use React / Vue / Angular / etc. I'm really excited about it, because I know it'll be so helpful for the people I have in mind. If you enjoyed this post, I think you'll love what I'm working on 😄

A front-end web development newsletter that sparks joy

My goal with this blog is to create helpful content for front-end web devs, and my newsletter is no different! It includes early previews to upcoming posts and access to special bonus goodies. No spam, unsubscribe at any time.