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:
How do you apply those properties to the
One way would be to use an interpolation function:
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:
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:
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.
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:
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:
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:
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
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
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:
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:
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.
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:
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:
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:
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.
Flourish component has a sibling with an in-between z-index, it’ll get “interleaved” between the bit and its background:
We can solve for this by explicitly creating a stacking context, using the
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.
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.
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:
It can also come in handy for components that can either render as buttons or links, depending on the circumstance:
Semantic HTML is very important, and the
as prop is a crucial bit of knowledge for all developers building with styled-components.
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:
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
By ”doubling down” on the class, its specificity increases.
.paragraph.paragraph is more specific than
This trick can be useful for increasing specificity without reaching for the nuclear option,
. 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.
In production, styled-components will generate unique hashes for each styled-component you create, like
.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:
If you use create-react-app, you can benefit from this plugin without needing to eject by changing all your imports:
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:
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.