How We Build a Component Library

How we implement a web UI component library with React, TypeScript, Bit and Design Tokens in our web design system

Jonathan Saring
JavaScript in Plain English

--

Visual consistency is often regarded as the “look and feel” of your digital products. However, in the modern world of web apps, the border between UI and UX is blurred and components are the key to both your visual language and your user’s experience.

When users encounter an inconsistent design or user experience they get confused, which in turn harms their desired goal completion rates. In simple words, UI/UX consistency is good for business. It also builds your brand, encouraging love and loyalty from users to your products and apps.

To breed such consistency organizations work to build a design system.

A design system has been traditionally built as a set of visual elements on a design tool like Figma, a style-guide, and a component library implemented in technolog like React and TypeScript. But that’s not a design system.

A design system is much more than that. It’s an economy of components in your organization, that helps everyone — designers, developers, component builders, product builders — collaborate and build faster and consistently.

In this post I’ll share how our own team is building a shared component system that we use as a foundation to build and design our products.

Developing the library: From a single package to a composable component system

When building a component library, you’re going to build a set of components that will change and evolve over time, and that will be distributed to other developers and teams for usage in their products and applications.

For most people, it starts with creating a new repo and building some components inside. Almost instantly, this turns into a “monorepo” which ironically is mainly used to solve code-sharing across other repos.

This post is the story of how we build a shared component system. We use open-source toolchain Bit to build and use it.

We embrace a different paradigm that puts the components first, not the repo. We leverage composability and the open-source toolchain Bit to develop and share components as independent entities.

That means that instead of a package with a set of components developers won’t adopt because they don’t want to give up control over their product’s roadmap and development, they will have a marketplace of components to find, use, and extend in their apps.

A composable paradigm is better for about 1000 different reasons; Modular software is better, more maintainable, easier to understand and fix, faster to build, simpler to test, quicker to scale, smooth to collaborate on, and more.

But in the context of a design system, it has one more huge advantage: It’s easier to adopt. And for design systems, adoption is everything.

Composability also unlocks many great workflows to solve and improve a variety of fields such as theming, tokens, and styling; We create themes with tokens that can be composed with “styless” components to create anything.

Here’s an example:

Look at our “base-react” scope of composable components.

These are the base-ui components we share, but without styles. To style them, we use themes and tokens from the “Design” scope.

Through composabilty we create a system of shared components that we can use across products to develop faster and to ensure consistency.

Even concrete features that rely on them can be shared and used in many different apps, since they’re all just (verioned, published) compositions.

Our library is more like “iTunes” than a hard-packed CD-Rom, which means our organization enjoys a live, growing, and collaborative economy of components that get adoption by happy developed across orgnizations.

How we develop and version components

We won’t go through how to start working with the component workspace but instead show you how easy it is to develop, share, and collaborate on components when you build them in a modular way.

Bit’s workspace let us dynamically create and fetch components we want to develop. It can be spawned on any folder regardless of which repo you’re working on. This is very useful for developing components.

In simple words: We build components like Lego, we don’t glue them together in one repo unless there’s a good reason to it.

New components can be developed using bit create or you can use bit fork to fork and modify existing components like this:

$ bit fork teambit.base-react/buttons/button

Every workspace tracks components in the bit.map file:

{
"buttons/button": {
"scope": "",
"version": "",
"mainFile": "index.ts",
"rootDir": "component-library/buttons/button"
},

At any moment you can run bit start to open the workspace UI and visualize the component, look at the cods, docs, versions, tests, examples etc.

You can also run bit status to see the state of all components in your workspace. Here’s a fictional example output of a quick status check on such a workspace:

$ bit statusstaged components
(use "bit export <remote_collection> to push these components to a remote Collection")
> buttons/button. versions: 0.0.1, 0.0.2, 0.0.3 ... ok
> layout/grid. versions: 0.0.1 ... ok
> pages/page. versions: 0.0.1, 0.0.2 ... ok
modified components
(use "bit tag --all [version]" to lock a version with all your changes)
(use "bit diff" to compare changes)
> navigation/link ... ok

You can run bit status at any time to get an overview of your Workspace: which components have been modified and are awaiting tagging, which are staged and awaiting export, and if any errors have been encountered.

It’s time to tag the first version of our button. Type the following command:

$ bit tag buttons/button -m "first version"

Running the tag command without specifying components will tag all modified components in your Workspace.

When tagging a component, compilation, testing, and building are done. This is true not only for the component you edited and tagged; Every component that depend on this component will also be tagged with a new version.

Travel back to older versions of any component; Rollback in production!

Versioning per-component is extremely useful for design systems. Read more here and here. The best thing you can hope for is adoption of your components by other teams in the organization; That won’t happen if all components are versioned together and people who install the lib will get a version update (triggering their project’s CI) on every irrelevant change.

Note: To share components across projects/developers you will need to host them in a remote scope. You can setup one on your own server, or use bit.cloud for free. Your choice.

How we handle component dependencies

Dependencies are the way components — and eventually applications — are composed together and integrated.

Bit streamlines the process of defining and updating dependencies for and between components in the workspace.

Here’s the button component example again, this time when clicking the “dependencies” tab view. As you can see, it imports the “link” component which in turn uses the “compare-url” component.

Whenever you modify a component and bit tag it with new version, all the components that depend on it the workspace will also be tested, built, and updated. This means “incremental” graph-driven builds will trigger for all components impacted by any change to a component they depend on.

For example in the above example if you’d be forking and modifying the “link” component and then run bit status you’ll see that “button” will also be pending a new version:

modified components
(use "bit tag --all [version]" to lock a version)
(use "bit diff" to compare changes)

> navigation/link ... ok

components pending to be tagged automatically (when their dependencies are tagged) > buttons/button ... ok

If you’d be using the bit.cloud then soon you can use Ripple CI (now in closed beta) which extends this propagation process across all components and teams in the organization to continuously integrate everything.

We already use it to update our shared components across our different projects. Here’s a sneak peek preview:

How we theme and style components

So here’s something cool; Use use theming and tokens to style components in a modular way like Lego. This means our components (see base-react scope above) basically have no style (no offense, components) — and our theming with design tokens components provides them with styling that can be easily changed or replaced across many components, pages, and applications.

Using a theme like this we can just “plug n play” themes and styles at will, making it very easy to designers to become active contributors as well (thanks to design tokens in our themes). Let’s see how to use a theme!

In a workspace (with some components like the button above) clone the theme and the theme provider:

$ bit fork teambit.base-react/theme/theme-provider$ bit fork teambit.design/themes/base-theme

Then just import the theme into the theme provider, and change the tokens to match your styles! Composability makes everything very simple.

How we build and test components

So this is a very interesting one.

In Bit, every component is developed in a “dev environment” which is also a component. That sounds complicated, but it’s really very simple.

What is means is that you can define how you want components to be compiled, tested etc and then reuse these configs easily.

Each component is developed, built, and tested (+linted etc) in isolation in its own environment. So when something fails, you can learn exactly where and what caused it. And, you only build the component that changed — nothing else (!) so builds can run up to 10X faster on an avg project.

Builds run on tags. Since Bit knows about the graph of all dependencies between components, it can make sure that when a component is tagged with a new version, that component and all its dependent components (propagating up the graph) will be tagged and built+tested as well.

Looking back at the above button example:

$ bit build --list-tasks buttons/buttonTasks List
id: nitsan770.component-library/buttons/button@0.0.5
envId: teambit.react/react

Build Pipeline Tasks:
teambit.harmony/aspect:CoreExporter
teambit.compilation/compiler:TSCompiler
teambit.defender/tester:TestComponents
teambit.pkg/pkg:PreparePackages
teambit.harmony/application:build_application
teambit.preview/preview:GenerateEnvTemplate
teambit.preview/preview:GeneratePreview

Tag Pipeline Tasks:
teambit.pkg/pkg:PackComponents
teambit.pkg/pkg:PublishComponents
teambit.harmony/application:deploy_application

Snap Pipeline Tasks:
teambit.pkg/pkg:PackComponents
teambit.harmony/application:deploy_application

The same goes for testing components.

You can test components using any tool you like (e.g. Jest) and define the tests as part of the tagging process so that whenever you make a change to a component, all impacted components up the dependency graph will also be tested running their own tests in isolation.

In fact, you can use:

bit test --watch

To make tests run on every change you make to your components in the local workspace and see it in the workspace UI so you can always tell which components (up the graph) might break on every change you make — during development time! Gives a new meaning to TDD doesn’t it?

Customizable docs that stay updated

So the next part is widely overlooked because most people think that adding visual examples or stories will do the trick and that’s it. It’s true that visual examples and stories are a great way to both develop and document components, but docs are actually much more than that.

  1. Docs must be a part of the component not external to it and should include things like props, tests, and an option to go back to older versions
  2. They must be updated as needed with every new version of the component (which is versioned in Bit)
  3. They should be inifnitly customizable and extendable and thanks to MDX can include any feature or other component in them.

That;s exactly what Bit does — as you develop components in your local workspace and run ‘bit start’ to open the workspace UI you can view the docs generated and updated live as you code.

You can add visual examples, prop tables, description, or in fact any other type of docs feature/component into the docs themselves.

Furthermore, after you share your components to a scope, these docs provide visibility and usability for components without having to create and maintain any external or additional documentation websites. And, they will be updated per-component with every new component version released. Sweet.

Here’s our real base UI component set in React we use as a foundation.

How we distribute and share components

Here things get even more interesting.

We don’t just build and publish a component library.

Truth be told, we build everything in components, frontend and backend alike. Teams own business responsibilities (‘search’, ‘ui’, ‘billing’ etc) and build them in components using Bit, while each team shares its components in a “Scope” — all our scopes are hosted on bit.cloud but can also be put on any other server just like Git. If you look at our scopes, you see exactly what features we build and which teams build them.

Furthermore, you can visualize the dependency relationship between all Scopes (and teams) by seeing who’s using who’s components on a graph.

If we look at the base-react scope — our styles UI component “library” — you can see it shares close connections with “design” which makes sense, that’s where all the themes are, and with other UI-heavy projects.

Here are a couple of more very cool scopes we have. Both are products that use components from our design system (base-react + design) as dependencies to create integration and collaboration.

  1. Community scope — The components we use to build our open-source community website at bit.dev. Naturally, this scope uses some of the components from base-react and from the design scopes as well.

2. The blog scope where we host all the components used to build out blog at bit.cloud/blog. We also use components from the design system there.

The scope is where components are:

  1. Hosted
  2. Shared
  3. Versioned and updated
  4. Made visible an discoverable
  5. Consumed and used from

You can share components directly to remote scopes using ‘bit export’, and then share and access them across many different projects and people.

You can create a remote scope on bit.cloud (free) and configure it in your workspace’s workspace.json file like this:

{
"teambit.workspace/workspace": {
"defaultScope": "your-username.demo-scope"
}
}

Later, ‘bit link’ can come in handy when changing scopes.

After a scope is created and defined, here’s an example of exporting the above components from you local workspace to a scope you created

$ bit exportexported the following 4 component(s):
nitsan770.component-library/navigation/link
nitsan770.component-library/themes/base-theme
nitsan770.component-library/theme/theme-provider
nitsan770.component-library/buttons/button=

Once in the scope, component can be used in many different projects — and can be updated with new versions from the scope!

How we discover, consume, and use components

Every component on the cloud has a ‘use’ button.

The ‘use’ button provides a verity of ways for developers to consume the component. For example, they can use install it as a package from the free bit.cloud registry or any other registry. They can also use ‘bit fork’ to fork the component into their workspace and edit it then tag a new version.

After you fetch a component into your workspace you can import and use it from any other component as well (!) — creating an infinite amount of compositions of different components extended and put together.

Here’s a very cool example of it (drag & drop the components):

Adoption adoption adoption

When you tell people “hi, use this package!” — they would like to, but they really can’t. Why? because they just can’t give up control over the development of their product.

Some of them even want to use the design system components; But what will happen if the PM of their product will require a change?

For that reason, DSs fight and struggle to get adoption.

Bit goes a long way for us in helping to create adoption for shared components:

A. By making it easy to install components using any package manager or bit, which also provides the option to “fork” components into a local workspace to use and make edits as needed — which really helps adoption.

b. When people just install one component they don’t add all that redundant weight to their projects and don’t get updates (that trigger CI) for code they don’t use. And, they don’t have to add redundant dependencies.

Component updates

Whenever the design system team wants to issue an update they can simply fork the component into their workspace and make changes to the code.

Then, all they have to do is tag a new version (note that bit will auto-tag all impacted dependant components as well) and export it to the scope.

Then when teams and developers that use the component — still on the old version — will run ‘bit install’ they will get all the latest version updates!

Simple right? Yet at scale this workflow can be tiring. And so, we’re currently using a new tool of our making called “Ripple CI” — which propagates updates to components across scopes, continuously integrating the organization…

Ripple is currently in closed Beta in the hands of our own team and some select organizations that use it to build together globally. It should be released later in 2022 with a free version as well.

Governance and collaboration

A component economy means you “democratize” and encourage the adoption and collaboration over components, while giving tools for regulation.

On Bit Cloud we can “verify” components we trust and recommend developers to use in their projects:

Bit also provides tools such as reusable templates called “dev environments” that help developers standardize the way (and tools) components are developed, built, tested, linted, documented, released etc.

We use many such tools and workflows, but for now I’d like to share two: One for governing components, and another for collaborating.

The first feature is “verified components” — It’s pretty straightforward, and it lets admins in the organization verify which components are ok to use.

The second feature is for suggesting changes and collaboration, and it’s called: “Lanes”.

Lanes are basically like Git branches only for a graph of components.

So if the “botton” depends on “link” and you want to change both, fork them into your workspace, open a lane, snap the changes, and export changes.

It’s as easy and simple as it sounds — if you have permission.

If you don’t, you can snap and send the suggested changes without a version for review.

That’s muuuch easier than going to another repository, diving into a huge complicated codebase, and eventually making a PR and then waiting.

Conclusion

A design system isn’t a repo with a bunch of components implemented inside it and a matching package.

It’s a living, vibrating, collaborative, changing, and highly composable set of components that you ultimately want people in your organization to discover, understand, choose, use, and ideally even contribute to.

That means you’re not just building a component library; You’re building a component economy. The sooner you realize that and aim to build it, the more useful your shared component system will be.

For us it means building a set of basic UI components in React which are all style-less, and at the same time building a set of similar components for themes (with design tokens) and styles. When people can combine and compose the two together, and with their own components, it becomes fun and simple for them to adopt the shared component system and build new things much faster while ensuring consistency in design and experience.

Thanks for reading and please feel free to comment or ask anything!

🍻 🍺 💁‍♂

--

--

I write code and words · Component-driven Software · Micro Frontends · Design Systems · Pizza 🍕 Building open source @ bit.dev