Build Better Component Libraries with Styled System
--
Introduction
Component-based design is an increasingly popular process for developing web interfaces. It was once common for organizations to rely on libraries such as Bootstrap or Material UI. But with the advent of improved tooling, many teams today are building their own themes, component libraries, and design systems. This influx of design and development collaboration is generating many new, exciting systems. That said, rolling your own systems comes with its own challenges and pitfalls. A recent library, styled-system
, is an effective tool for navigating some of these issues.
This is an admittedly vast topic riddled with rabbit holes. So, Iâd like to limit the scope of this article to four common problems Iâve found in less mature component libraries and how styled-system
can help solve them.
Before we dive in, Iâd like to say thank you to Brent Jackson, John Otander, and everyone else who contributes to styled-system
. đ
A Brief Overview of Styled System
Note: This post is not a technical overview of
styled-system
. Varun Vachhar has already written an excellent article on that.
If youâre not familiar, styled-system
is a library for adding theme-based style props to components using CSS-in-JS and React. It accomplishes this via style functions, and allows you to build components like this:
// from styled-system's README
import styled from 'styled-components'
import { space, width, fontSize, color } from 'styled-system'// Add styled-system functions to your component
const Box = styled.div`
${space}
${width}
${fontSize}
${color}
`
and implement them like this:
<Box m={3} p={2} bg="palevioletred">
Hello, World!
</Box>
Note: These examples are using styled-components, but you could also use other CSS-in-JS libraries such as Emotion.
The library is very simple on its face, but it provides a powerful API that solves a lot of common component library problems. Letâs take a closer look at how.
An Ideal Structure
Components living in a shared library are inherently different from most application-specific components. While they may have different levels of reusability, they should be designed to look and behave consistently regardless of their application context. Also, they are generally created with reusability and extensibility in mind. I like to use this diagram to describe different component types:
Example components for the diagram above:
- Low complexity & High reusability: Buttons, Text, Labels & Grid
- Medium complexity & Medium reusability: Input fields with labels and validation text
- High complexity & Low reusability: Date picker
While your client applications probably have more context-specific components and fewer reusable components, a shared library should be the opposite. You generally want to have more small, interchangeable components that you can combine in different ways to create more complex components. As component complexity increases, reusability decreases. Because of that, we want to maximize our least complex components. While this is the ideal, itâs common for libraries to adopt patterns that work against it.
Common Problems & Styled System Solutions
Itâs important to note that the problems listed below usually donât cause components to fail dramatically. Thatâs part of the reason they are so common. Instead, they gradually introduce friction into the system. Components slowly become more challenging to use and extend, and the library becomes more difficult to maintain. Updates become more time-intensive and side-effects are less predictable.
Note: Brent Jackson also discusses several of the issues mentioned here.
Second-Class Theming
If youâre not familiar, CSS-in-JS libraries such as styled-components or Emotion, a theme object is passed to a Provider
component, which makes the theme's values available to any component using those libraries. Often, values for colors, font sizes, and spacing live in this theme. Theming is one of the most powerful tools in CSS-in-JS libraries, but it is also often underutilized.
While theme values are passed in as props, they can be overlooked. A developer unfamiliar with the theme could easily use one-off styles for spacing instead of using theme-based values. Or, perhaps they add a custom color or font-size without referring to documentation.
Styled System, in contrast, makes the theme a first-class citizen:
<Card p={2}>
Card Text
</Card>
By adding spacing props to Card
, we make it very simple for developers to hook into theme-based values by default. There's no need to inspect the theme object or to read documentation explaining the reasoning behind the spacing scale. The best way to encourage and maintain consistency is by making the right choices easy.
Related to this, styled-system
âs style functions are designed to use scalar values. Many of the building blocks of a design system are scales: spacing, font sizes and weights, colors, shadows, motion, etc. By incorporating scales into its style functions, styled-system
makes it really simple to navigate along theme scales without having to remember particular values.
Using our example from above:
<Card p={2}>
Card Text
</Card>
Developers can easily move two steps up from the base padding value without having to know the particular value.
Inconsistent Props
Another common problem is inconsistent prop names and behaviors across components. For example:
<ButtonSecondary thin />
<ButtonPrimary type=âthinâ />
These two components could have been created by different developers, or perhaps the same developer who decided to change the API over time. Itâs difficult to know what thin
is doing in either component without consulting the documentation. And it's even more challenging to know whether thin
behaves the same in ButtonSecondary
and ButtonPrimary
. Documentation is essential to building a quality library, but these inconsistencies introduce friction. A good system should teach people how to use it as they interact with it.
Note: Weâre very fortunate to have a great ecosystem of documentation tools for components. If youâre unfamiliar, check out react-styleguidist, storybook, MDX JS, and Docz.
Keeping prop names and behaviors consistent across components is very challenging to do on your own. But Styled System solves this with its built-in consistent style functions:
<ButtonPrimary p={2} />
<ButtonSecondary p={2} />
After using p
once, I can know its behavior. This allows me to have confidence if I need to update it, and I can use p
across any component using the space
style function.
Note: There is definitely a use case for props such as
type=""
, but I would recommend limiting those to component-specific appearance and behavior alterations.
Consistent component APIs may seem like a small issue, but it becomes increasingly important as your library grows.
Lack of Component Hierarchy
A third problem I often see is a lack of component hierarchy. Instead of a pyramid structure in the diagram above, most, if not all, components are siblings and have no smaller, shared components. This lack of hierarchy creates duplication and makes predictable component updates very challenging.
Note: If youâre not familiar with âcomponent hierarchy,â I would recommend reading Brad Frostâs Atomic Design and Subatomic Design Systems by Daniel Eden
For example:
<Text>This is a card</Text>
<Card>
<Card.Text>
Hello!
</Card.Text>
</Card>
If Card.Text
isn't extending Text
, we now have to update both to keep them in sync. In contrast, styled-system
's API encourages you to first build small, modular components that can be easily extended and modified.
Lack of Component Extensibility
Related to all of the above, I often see problems with extensibility. Either the component API is too rigid or the componentâs internal complexity makes it too fragile to predictably modify or extend.
Styled Systemâs API is built to be highly extensible by default. If Card
has a padding of 2 by default, I can easily modify it for a particular use case:
<Card>
<Card.Text>
Default Card
</Card.Text>
</Card>
<Card p={4}>
<Card.Text>
Card with Deluxe Paddingâ˘
</Card.Text>
</Card>
It also lends itself to building our simplest components first. If we already have Card
built, we can easily create a more complex component, ProfileCard
on top of it. This is also something we get from CSS-in-JS libraries, but styled-system
makes the extension more declarative.
Conclusion
Thanks for reading! I hope you found this helpful. Styled System provides a simple API for solving common component library problems. It reduces friction in your design system by tightly coupling components to the theme, provides a consistent API across all components, and keeps components extensible. If youâre starting a new React component library or looking for a way to improve an existing one, Iâd highly recommend trying it out.
You can find me on Twitter and GitHub. If you enjoyed this, you should also subscribe to my newsletter!
References
Libraries
Resources
- Component-based Design
- Adele Design System Repo
- Component-based Design with Styled System
- How Styled System works
- Defining Component APIs in React
- Atomic Design
- Subatomic Design Systems