Build Better Component Libraries with Styled System

Thank you to my friend Alan Smith for writing this article on components. The man is amazing, professionally and personally.

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:

component types diagram

Example components for the diagram above:

  • Low complexity & High reusability => Buttons, Text, Labels, Columns, Rows
  • 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, interchangable 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>

A developer 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 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 it's 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

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

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.

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 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.

References

Libraries

Resources

People

The contributors to JavaScript January are passionate engineers, designers and teachers. Emily Freeman is a developer advocate at Kickbox and curates the articles for JavaScript January.