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 Wrapper
?
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:
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:
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
:
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:
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.
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:
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.
If our 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 isolation
property:
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.
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 .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, . 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 .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:
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.
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 đ