In September of last year, I was assigned to a task force within the Quipper product team. We were formed to deploy a new app to market in roughly three months. Given the tight timeline, agility was a top priority, so every engineering decision had to be carefully considered.
I took it upon myself to prepare a styling framework for the React app we would be building. I was curious to explore a new CSS-in-JSS styling methodology I discovered, called Styled System. The project had over 5,000 stars on GitHub and apparently GitHub themselves used it to build their own design system.
The CSS-in-JS movement was alive and well by this time, but it was something I was lukewarm to because I hadn’t ever really used it at scale. Outside the JavaScript world, I’ve settled on writing my CSS the Atomic way because it’s served me very reliably through all my previous projects. I wanted a React-y way to do something similar.
<h1 class="text-lg font-bold text-center">
I'm being styled with atomic CSS!
</h1>
An example of Atomic CSS (done with Tailwind CSS)
If you’re not familiar with, or even a fan of, Atomic CSS, I’d encourage that you read this blog post by Adam Wathan—host of the excellent Full Stack Radio podcast—because it chronicles our journey as an industry towards Atomic CSS and the rationale behind it. (I find that it closely parallels my own journey with CSS.) Styled System follows those same ideologies, so naturally, I had to build out the entire styling framework of our app with it. (Thanks for letting me run wild, team!)
A quick primer on Styled System
Styled System is a props-based styling methodology, meaning you style components by passing in styles as props (called style props):
<Text color="body" fontSize="2">
Hello, Styled System!
</Text>
It looks a little like Atomic CSS! Awesome! (Or like inline CSS, but those style rules are applied to your component via auto-generated classes, so they don’t actually create the same issues with specificity.) But, one key difference is, being just plain CSS, Styled System doesn’t require that you memorize different utility class names to apply the styles you want. You use plain old regular (albeit camelCased) CSS.
Take note though that the values being passed in aren’t your typical CSS values. "body"
is not a valid CSS color name and "2"
not a valid value without a corresponding unit. These are actually theme values taken from a global theme object defined at the top level of your application:
const theme = {
colors: {
body: "#1e3f6b",
},
fontSizes: [12, 14, 16, 20],
};
color="body"
points to theme.colors.body
while fontSize="3"
is theme.fontSizes[3]
You can use this to constrain styles within a particular set of rules, like say a brand style guide or a design system. This way, your components can be made to follow the specifications handed to you by your designers (and they don’t have to scold you for being 1 pixel off, again).
Though to me, the main advantage to styling components this way is how it enables rapid development. Previously, we’d have to write our markup, then open a separate file to manage all our styles, which can become a tiresome exercise in context switching. The worst part of that system though—and it may seem trivial, but really it isn’t—is having to come up with appropriate class names each time.
Sure, it’s easy if we’re talking about naming the primary button on your site, but how about when we’re trying to target a specific button in a specific context within a specific page?
There are only two hard things in Computer Science: cache invalidation and naming things. —Phil Karlton
But even if Styled System allows us to get away with those things, you’re likely not entirely convinced at this point. The number one thing on your mind right now might be:
Still, why would I want all my styles in my HTML? What is this madness?!
—which is a fair point and one the other engineers on the team weren’t shy of letting me know. But remember that because all this is happening in JavaScript, it can be easy to abstract away common patterns. If say, a heading used across the site needs a particular set of styles, it wouldn’t be ideal to have to write them over and over! You can actually create a component with all those base style rules passed in by default:
// Heading.js
const Heading = ({ children, ...props }) => <Text {...props}>{children}</Text>;
Heading.defaultProps = {
color: "body",
fontSize: "3",
fontWeight: "bold",
};
The idea then is that instances of <Heading>
will only need to be given context-specific styles like margin
or textAlign
. This way, the styles for headings appearing within larger contexts will only be minimal and all the complex styling can remain in the underlying components.
// ArticleBlock.js
import Card from "./Card";
import Heading from "./Heading";
const ArticleBlock = () => (
<Card>
<Heading mb="3">Is Styled System the future?</Heading>
{/* ... */}
</Card>
);
Styled System also supports property shorthands like mb
, short for marginBottom
You can also opt to solve this problem using the Styled System variants API. Either method works, but my philosophy has been to use variants
for rules specific to the design and components for those specific to the app.
It wasn’t all perfect
All that being said though, style props still became an issue for our team because, even if we were able to limit context-specific styles to no more than 3 lines of props, some components would still require many more of their own props aside from that. This became an ugly mess for components that required a large mix of style and logic props:
<Input
flex="1"
mt="2"
ml="3"
type="number"
placeholder="--"
value={score}
required={hasCorrespondingCriteria}
disabled={!hasCorrespondingCriteria}
onChange={handleFormChange}
/>
An unfortunate example from our codebase
This was our biggest gripe with Styled System because it was difficult having to deal with styling and logic on the same level. When working with Atomic CSS, all the styles are at least confined under a single className
prop, so the problem isn’t as pronounced there.
To address this issue, we thought at first about defining all the styles in separate objects at the top of each file, then spreading them onto each component, like so:
const scoreInputStyles = {
flex: 1,
mt: 2,
ml: 3,
};
/**
* Somewhere further down the file
* ...
* ...
*/
<Input
{...scoreInputStyles}
type="number"
placeholder="--"
value={score}
required={hasCorrespondingCriteria}
disabled={!hasCorrespondingCriteria}
onChange={handleFormChange}
/>;
But that would eliminate the advantages we talked about earlier! We’re having to move up and down the same file just to define styles. But most of all, who wants to go back to naming things again?!
I then realized that we could just skip the initial declaration by defining the object inline, then spread it directly onto our components like so:
<Input
{...{ flex: 1, mt: 2, ml: 3 }}
type="number"
placeholder="--"
value={score}
required={hasCorrespondingCriteria}
disabled={!hasCorrespondingCriteria}
onChange={handleFormChange}
/>
Using object notation for your style props
Great! Now the style props can appear visually distinct from the rest of the props. This will make it much easier to parse through component files when wanting to focus on programming just business logic.
Our bigger issue
That wasn’t the end of it though. We also had problems with style props not always working when applied to certain components. Ironically, this was something that occurred by design because Styled System actually recommends designing your base components to limit the style props they will accept.
const Text = styled.span(
({ theme }) => css`
color: ${theme.colors.text};
font-size: ${theme.fontSizes.body}px;
font-family: ${theme.fonts.main};
line-height: ${theme.lineHeights.main};
`,
color,
space,
typography
);
The initial <Text>
component declaration in our app
The arguments passed in at the end (color
, space
, and typography
) are what are called style prop functions. They dictate the style props that your components will respond to. Each “allows the passage” of their own group of CSS properties. Something like border="5px solid black"
therefore, won’t work when applied to our <Text>
component because that would require the border
style prop function. But we can apply color
, padding
, margin
, and type styles like fontWeight
and others.
The intent is to prevent components from deviating from their intended design—which is a reasonable argument—but it slowed our team down more than anything! Styles sometimes didn’t just work. And this happened often enough that after about the nth time or so, I realized that the whole thing is more trouble than it’s worth. We wouldn’t be applying these styles if they didn’t need to be there one way or another!
To get around this problem, the documentation suggests two possible solutions—neither without their quirks. The first is to extend your components via the styled
function of the styled-components
library, then apply any additional styling through there, but this created the same issues as with defining objects like we did earlier.
import styled from "styled-components";
const CustomButton = styled(Button)`
background-color: transparent;
float: right;
`;
/**
* Somewhere further down the file
* ...
* ...
*/
<CustomButton>Download</CustomButton>;
Scroll, scroll, scroll, scroll
Alternatively, styled-components
also provides a css
prop that will allow you to inline styles on any CSS property of your choosing, but it creates a messy API for our components because it leaves half your styles inside the css
prop and half outside. How can we tell when to use which? Talk about confusing!
<Text
{...{ textAlign: "center", fontSize: 2 }}
css={{ flexGrow: 1, justifySelf: "flex-end" }}
/>
The bigger issue here though is that theme values no longer work inside the css
prop, which basically brings us down to the level of writing inline styles—yikes! Fortunately, Styled System has an external css
function helper package, which addresses just that issue. It opens us up to the core functionality of Styled System without the arbitrary constraints.
Now, we can have the benefit of applying styles to any property (through the css
prop) with the ability to use theme values at the same time (via the css
function)!
Getting there…
import css from "@styled-system/css";
<Text css={css({ color: "body" })}>{"I'm color #1e3f6b!"}</Text>;
css
prop + css
function = ✨
From our experience, the best way to go is to pair *the styled-components
css
*prop with with the styled-system
css
*function* and just leave style props by the wayside. Not only do we have themed CSS by styling our components this way, but—going back to our first issue with style props—because everything is confined to a single prop, styling and logic can still also remain separate.
The syntax feels a bit redundant right now, but we can fix that by abstracting the css
function inside of our component declarations. Therefore, instead of defining your components the way we did earlier, write them like this instead:
const Text = ({ css: contextStyles, children, ...props }) => (
<span
css={css({
color: "text",
fontSize: "body",
fontFamily: "main",
lineHeight: "main",
...contextStyles,
})}
{...props}
>
{children}
</span>
);
First, pass in the default styles, then layer any of the provided styles on top through the css
function
The Holy Grail?
Did you catch all that? Now, we can write our styles like this:
<Text css={{ color: "body" }}>{"I'm color #1e3f6b!"}</Text>
At this point, we might not even need the main styled-system
package and could get away with just @styled-system/css
. We’d still need several of the utilities from styled-components
(like the css
prop), but consider it a win to be able to drop the main dependency altogether and rely on just Styled System’s core functionality! (And if you’re a bit more advanced and are wondering, yes, this does still allow us to use Styled System’s array props for responsive styles.)
Unfortunately for our project, I only figured all this out after we had shipped, but if we were to go through it all again, I would have done it this way 100%. This set-up, while still preserving the core of Styled System, would also have saved us our biggest gripes with it. No mixing of style and logic props. No more arbitrary style prop constraints.
Just simple, isolated, and reliable styling.