Building a UI Component Library with Styled Components

JW

Jonathan Wong / February 12, 2019

6 min read––– views

Building a UI Component Library with Styled Components

Don’t repeat yourself. It’s a principle that engineers strive to adhere to — preventing code duplication by abstracting shared functionality out into its own place.

At Hy-Vee, we reached a point where we were spinning up new teams extremely quickly and needed to maintain consistency across products. How could we ensure every person created a button that looked and functioned the same way across all digital properties?

Our first step in tackling this problem was having UI/UX define a style guide that all consumers should implement. We needed a simple approach to ensure the adoption of our style guide and a set of shared components consumers could use without having to worry about styling.

Developing a Plan#

We decided to build a reusable UI component library which will be consumed in all of our client-facing applications. This project had a few main goals:

  1. Create consistency across the organization

    • All digital properties should look and feel the same, implementing our agreed upon style guide.
  2. Improve the overall quality of our codebase

    • Having a shared set of UI components means less custom code for consumers.
    • We can ensure all components meet our accessibility requirements.
    • Components become hardened by multiple teams contributing to bug fixes and improvements.
  3. Increase developer proficiency

    • Developers not specialized in CSS don’t have to worry about CSS quirks or cross-browser issues.
    • Allow us to ship new products and rewrite legacy products faster.
    • Decrease the amount of time it takes new employees to build UIs correctly.

To accomplish these goals, we chose to use styled-components as the base of the library. styled-components uses ES6 tagged template literals and CSS to allow you to write real CSS code in your components instead of using JS objects.

It’s used by companies like Bloomberg, Atlassian, Reddit, Patreon, Target, Coinbase, and more. There’s a variety of CSS in JS solutions, as outlined very succinctly here. After reviewing this list and reading the styled-components documentation, it seemed like a no-brainer for our use case. Some big wins for us:

  • Feels like writing traditional CSS vs. JavaScript objects
  • React & React Native support
  • Auto-prefixing vendor styles
  • Scoped styles eliminate global CSS conflicts

Building the Library#

Let’s take a look at a simple example: a button.

const BaseButton = styled.button`
  border-radius: 4px;
  cursor: pointer;
  font-size: 1rem;
  padding: 8px 32px;
`;

If it looks familiar, that’s because it should. It’s truly CSS without anything extra on top. Using the template string, it invokes the styled.button function and passes in the CSS string. This button makes it extremely easy for consumers to use (as shown below).

<BaseButton>{'Hello World!'}</BaseButton>

A component library doesn’t have just one type of button, though. Let’s explore how we can extend this BaseButton component.

const Button = styled(BaseButton)`
  background: #e21c11;
  border: none;
  color: #fff;

  :hover,
  :focus {
    background: #af0000;
  }
`;

We can pass the existing BaseButton into styled to build our primary colored button using our main brand color.

Using Props#

Extending isn’t the only way we can modify our base component. We can also conditionally change styles based on the props passed in. Let’s explore adding a disabled state to our Button component.

${(props) => props.disabled && css`
    opacity: 0.25;
    cursor: not-allowed;
`}

Then, we can pass in disabled as a prop when invoking the component.

<Button disabled>{'Hello World!'}</Button>

Utilizing Javascript & React#

There’s so much more we can do with our components.

Let’s look at a more complicated example. We want to build an Input component, but abstract away the tricky parts for consumers. All inputs should have a label, which needs to reference the actual <input> element by ID. We also need to ensure the proper aria-labels are added for accessibility.

const Input = ({ disabled, id, label, placeholder, required }) => (
  <Container>
    <Label htmlFor={id} required={required}>
      {label}
    </Label>
    <Input
      aria-label={label}
      aria-required={required}
      disabled={disabled}
      id={id}
      placeholder={placeholder}
      type="text"
    />
  </Container>
);

Now, it’s much easier for consumers to properly build accessible forms with the correct styling.

<Input
  disabled={false}
  id="first-name"
  label="First Name"
  placeholder="Please enter your name"
  required
/>

Testing#

We utilize Jest for Snapshot testing. This works particularly well with this library because it’s presentational. Our snapshots are expecting the visual properties (i.e. CSS styling) are being correctly conditionally applied.

For each permutation of a component (e.g. different props), we have a separate snapshot such that all conditions are covered. Snapshot output is made more readable using jest-styled-components. For more information, read Effective testing of styled-components with Jest Snapshots.

Snapshots do not cover every scenario, however. If we were to add onClick logic to a Button for example, we would want to utilize traditional testing methods to ensure the correct actions happen.

Distributing#

Using Babel and Webpack, we can compile and generate a dist that can be published to NPM.

Thanks to babel-plugin-styled-components, we get these (and more) for free:

  • Generated class names are prefixed with the file and component name for an improved debugging experience.
  • React Developer Tools shows Button instead of styled.button.

Here’s our .babelrc file.

.babelrc
{
    "presets": ["@babel/preset-env", "@babel/preset-react"],
    "plugins": ["babel-plugin-styled-components"]
}

Contributing#

We wanted to make it as easy as possible for others at Hy-Vee to contribute. That’s why all formatting and linting is taken care of with:

Thanks to git pre-commit hooks with husky, we’re ensured all code is formatted correctly before getting pushed to GitHub.

IDE Integration#

There are even extensions for your favorite IDE to have CSS syntax highlighting inside of the JS template strings. Thanks, styled-components! 🎉

Storybook#

Storybook provides us with an interactive UI playground for our components. This makes development a breeze and also allows us to publish our Storybook to GitHub Pages using the Storybook Deployer. That way anyone can explore the components and see consumption examples without having to dive into the code.

Another bonus feature of using Storybook are its add-ons. You’ll notice in the bottom pane we’ve chosen to use:

  • Story Source for showing code consumption examples
  • Viewport for allowing us to test different screen sizes and devices
  • a11y for ensuring our components are accessible
  • storybook-readme so our documentation lives alongside the component
  • addon-knobs so we can dynamically edit props

Storybook Example

Future Plans & Roadmap#

We’re working to have this library adopted across all web and mobile applications at Hy-Vee.

As we’ve built out this library, it’s prompted us to think deeply about our component architecture and strategy. We’re working to define what set of components makes sense for all consumers to implement and which should be their own separate entities.

This has inspired us to implement a Monorepo design where we can easily develop and publish a large number of packages.

Want to learn more? Read the next post where I'll create a Monorepo with Lerna and Yarn Workspaces.

Discuss on TwitterEdit on GitHub
Spotify album cover

/tools/photos