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:
- 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:
- 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.
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).
A component library doesn’t have just one type of button, though. Let’s explore how we can extend this
We can pass the existing
styled to build our primary colored button using our main brand color.
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
Then, we can pass in
disabled as a prop when invoking the component.
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.
Now, it’s much easier for consumers to properly build accessible forms with the correct styling.
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.
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
"presets": ["@babel/preset-env", "@babel/preset-react"],
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.
There are even extensions for your favorite IDE to have CSS syntax highlighting inside of the JS template strings. Thanks,
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:
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. Stay tuned for our next post where we’ll talk about creating a Monorepo with Lerna and Yarn Workspaces.
If this library sounds interesting, come work with us! We’re working to invent the future of grocery, one component at a time.