When I first started using styled-components, it seemed like magic ✨.
Somehow, using an obscure half-string-half-function syntax, the tool was able to take some arbitrary CSS and assign it to a React component, bypassing the CSS selectors we've always used.
Like so many devs, I learned how to use styled-components, but without really understanding what was going on under the hood.
Knowing how it works is helpful. You don't need to understand how cars work in order to drive, but it sure as heck helps when your car breaks down on the side of the road.
Debugging CSS is hard enough on its own without adding in a layer of tooling magic! By demystifying styled-components, we'll be able to diagnose and fix weird CSS issues with way less frustration.
In this blog post, we'll pop the hood and learn how it works by building our own mini-clone of 💅 styled-components.
Link to this headingThe big idea
Let's start with a minimal example, taken from the official docs:
styled-components comes with a collection of helper methods, each corresponding to a DOM node. There's
button, and dozens more (they even support SVG elements like
h1 is a helper method on the
styled object, and we call it with a single argument, a string.
These helper methods are little component factories. Every time we call them, we get a brand-new React component.
Let's sketch this out:
When we run
const Title = styled.h1(...), the
Title constant will be assigned to our
NewComponent component. And when we render the
Title component in our app, it'll produce an
<h1> DOM node.
What about the
styles parameter that we passed to the
h1 function? How does it get used?
When we render the
Title component, a few things happen:
- We come up with a unique class name by hashing
stylesinto a seemingly-random string, like
- We run the CSS through Stylis, a lightweight CSS preprocessor.
- We inject a new CSS class into the page, using that hashed string as its name, and containing all of the CSS declarations from the
- We apply that class name to our returned HTML element
Here's what that looks like in code:
If we render
<Title>Hello World</Title>, the resulting HTML will look something like this:
Link to this headingLazy CSS Injection
In React, it's common to render some JSX conditionally. In this example, we only render our
<Wrapper> element if our
ItemList component is given some items:
It may surprise you to learn that styled-components doesn't do anything with the CSS we provided in this case. That
background declaration is never added to the DOM.
Instead of eagerly generating CSS classes whenever a styled component is defined, we wait until the component is rendered before injecting those styles into the page.
This is a good thing! On larger websites, it's not uncommon for hundreds of kilobytes of unused CSS to be sent to the browser. With styled-components, you only pay for the CSS you render, not the CSS you write.
styled.h1 has its own little scope that holds the CSS string. When we render our
Wrapper component, even if it's seconds/minutes/hours later, it has exclusive access to the styles we've written for it.
There's one more reason that we defer CSS injection: because of interpolated styles. We'll cover those near the end of this article.
Link to this headingDynamically adding CSS rules
You might be wondering: how does that
createAndInjectCSSClass function work? Can we really generate new CSS classes from within JS?
We can! One straightforward way is to create a
<style> tag, and then fill it with raw CSS text:
For a long time, styles generated through the CSSOM couldn't be edited in the Chrome developer tools. If you've ever seen "greyed out" styles in the devtools, it's because of this limitation:
Thankfully, however, this changed in Chrome 85. If you're interested, the Chrome team wrote a blog post about how they added support for CSS-in-JS libraries like styled-components to the devtools.
Link to this headingGenerating helpers with functional programming
Earlier, we created an
h1 factory function to emulate
This works, but we need many many more helpers! We need buttons and links and footers and asides and marquees.
There's another problem, too. As we'll see,
styled can be called as a function directly:
Let's update our styled-components clone to support these new requirements. We can borrow some ideas from functional programming to make it possible.
This code uses a technique known as currying. It allows us to "preload" the
If you haven't seen it before, currying can be a bit mindbending. But it's helpful in this case, as it allows us to easily create many shorthand helper methods:
For more information on currying, check out this lovely blog post by Aphinya Dechalert.
Link to this headingWrapping custom components
One of the coolest things about styled-components is that we can mix them with our own custom components!
Here's an example:
At first glance, this seems absolutely magical. How are we applying those styles to our custom component?
The key is in the prop delegation. Here's what we'd see if we logged out that
className coming from? Well, we create it inside our styled helper!
Here's the order of operations:
- We render
UrgentMessage, a styled component that composes the
- We come up with a unique class name (
OkjqvF), and render the
Messagecomponent) with a
Messagegets rendered, passing the
classNameprop to the
This only works if we apply the
className prop to an HTML node inside our component though. The following won't work:
By delegating all of the props that
Message receives to the
<p> element it renders, we unlock this pattern. Thankfully, many third-party components (eg. the
Link component in react-router) follow this convention.
Link to this headingComposing styled-components
In addition to wrapping our own components, we can also compose styled-components together.
I used to think that the library "merged" these styles somehow, creating a new "mega class" that contained all the declarations. But it actually creates two distinct classes.
If we check out the HTML/CSS generated, it would look something like this:
Our goal in this process is to make sure that the
PinkButton styles "extend"
Button styles. In the event of a conflict,
PinkButton should win.
In CSS, there is a complex hierarchy of rules that governs how to resolve conflicts. An ID selector,
#btn, will win out against a class selector,
.btn. Which in turn beats a tag selector,
But in this case, we have two classes! The two selectors,
.def456, are equally matched. So the algorithm falls back to a secondary rule. It looks at the order of rules defined in the stylesheet.
.def456 is defined after
.abc123, it wins. Our button will be pink.
An important clarification: The order that we apply the classes doesn't matter. Consider this case:
You might expect that the first paragraph would be red, and the second paragraph would be blue. But that would be incorrect: both paragraphs will be blue. The order of the classes in the
class attribute doesn't matter:
The styled-components library takes great care to ensure that CSS rules are inserted in the correct order, to make sure that the styles are applied correctly. This is a non-trivial problem: we do all kinds of dynamic things in React, and the library is constantly adding/removing classes, while ensuring that a sensible order is preserved.
Alright, let's keep working on our styled-components clone.
We need to update our code so that it applies both of the classes. We can combine them like so:
When we render
Tag variable is equal to our
PinkButton will generate a unique class name (
def456), and pass that as the
className prop to
If you're confused by this process, you're not alone; we've entered the mindbending realm of recursion, where styled-components are rendering styled-components. As I wrote this article, I tripped over this bit, and had to spend a minute reconfiguring my own understanding.
But here's the thing: the exact JS mechanics that accomplish this aren't important. That's an implementation detail. The important thing is that you understand these takeaways:
- When we render
PinkButton, we're also rendering
- Each styled component will produce a unique class, like
- All of the classes will be applied to the underlying DOM node.
- styled-components makes sure to insert these rules in the correct order, so that
PinkButton's styles will overwrite any conflicts in
Link to this headingInterpolated styles
We've almost finished creating our minimum-viable styled-components clone, but there's one more task in our list: .
Sometimes, our CSS will depend on React props. For example, an image might take a
Here's what our DOM looks like, after rendering these images:
The first class,
sc-bdnxRM, is used to uniquely identify the React component that was rendered (
ContentImage). It doesn't provide any styles, and we can ignore it for our purposes.
The interesting thing is that each image is given a completely unique class!
The seemingly-random class names,
eXyedY, are actually hashes of the styles that will be applied. When we interpolate in a different
maxWidth prop, we get a different set of styles, and so a unique class is generated.
This explains why we can't "pre-generate" the classes! We have to wait until the component is rendered before we know what CSS will be applied, because the same
styled.img instance won't always produce the same styles.
Interpolation isn't the only way we can customize the styles of one particular component instance. My personal favourite way is to use CSS variables. It looks like this:
If we inspect the HTML, we'll notice that both elements share the same CSS class:
By letting modern CSS do the dynamic stuff for us, we produce less CSS. This is also a potential performance win: when the dynamic data changes, we don't need to generate a whole new CSS class and append it to the page!
I wrote about this pattern (and many others!) in my blog post, “The styled-components Happy Path”.
Link to this headingCorrecting the record
Remember earlier, when I said you could think of these two things as equivalent?
When we start supporting interpolations, they stop being equivalent.
Tagged template literals are wild, and it would take an entirely separate blog post to explain how they work and what they're doing here.
The important thing to know is that all of those little interpolation functions are called when the component is rendered, and used to "fill in the gaps" in our style string.
Here's a quick sketch:
When we render our component,
reconcileStyles is able to invoke each of those interpolated functions with the data passed through props. In the end, we're left with a plain ol' string with populated values.
When the props change, the process is repeated, and a new CSS class is generated.
Link to this headingGood golly, you made it to the end!
This blog post has been a journey. Perhaps more than any other post I've written, this one covers some pretty treacherous terrain. This stuff is not easy to wrap our mind around.
If parts of this post didn't make much sense to you, that's alright! It might require a bit of percolation. As you continue to use styled-components, hopefully the ideas in this post will become clear.
In addition to the practical benefits of understanding your tools, I hope that this post also helps you appreciate what the styled-components team has accomplished. Building and maintaining open-source software is never easy, and it's especially fraught when working on a CSS-in-JS tool, in a community where the entire category is contentious.
I'm planning to launch the course in September of this year. You can sign up for updates on the official site!