Building a Composable Material UI Design System with Bit

How to create a composable MUI based design system that is easy to maintain, easy to adopt and easy to extend

Nitsan Cohen
Bits and Pieces

--

Material-UI is a renowned open-source UI component library that marries the aesthetics of Material Design with the functionality of React. Its versatility has become a favored choice for numerous enterprises looking to establish their design system.

Essentially, the ultimate goal of developing a design system is its widespread use. Experience shows that users gravitate towards composable software rather than intimidating monolithic structures.

TL;DR

See my collection of independent Bit components. Head over to the ‘design’ scope, to explore my design system. See my app component in its scope. Examine my app’s dependency graph, which “goes all the way down” to my design components. See my demo app in production.

What is a composable design system?

A composable design system is much like a collection of building blocks. Each block, or in this case, a component, can be picked and used independently. It signifies the flexibility and freedom to construct a digital product using the parts that perfectly fit your project’s needs. Imagine having a vast library of elements at your disposal where you can handpick precisely what you require.

My ‘design’ scope hosts independent Bit components for the design system

One of the primary benefits of a composable design system is its ability to avoid redundant updates. Unlike a monolithic structure where a minor update could mean overhauling the entire system, a composable design system allows updates to be carried out at the component level. This equates to faster, more efficient updates, and less time spent on managing the system.

Moreover, a composable design system offers the capacity to extend and customize UI components, design tokens, and themes. It empowers developers and designers to not only use but also modify and enhance components according to their unique requirements. It bridges the gap between standardization and customization, providing a fine balance that caters to both consistency and innovation.

The ‘portfolio’ scope with portfolio-app components, hosts a custom theme that extends the default one

To bring all these qualities to life and create a truly composable design system, we’re going to use Bit.

Bit ensures that every part of your design system, every component, can be used, tailored, and extended independently, promoting flexibility and efficiency in software development. In the following sections, we’ll dive into how Bit accomplishes this feat, facilitating a composable design system like no other.

Our design system’s inventory

Tokens

Tokens in a design system can be thought of as the foundational elements. They are atomic units like color, typography, spacing, and shadows that dictate the basic visual and functional aspects of a design system. These attributes are utilized across various components, providing a consistent look and feel throughout your application. With tokens, you can manage and apply design decisions en masse, making your design system easier to maintain and evolve.

{
overline: {
fontSize: '0.75rem',
fontWeight: 600,
letterSpacing: '0.5px',
lineHeight: 2.5,
textTransform: 'uppercase',
},
caption: {
fontSize: '0.75rem',
fontWeight: 400,
lineHeight: 1.66,
}

For simplicity, this demo does not include an independent tokens component, but rather, implemented directly using the MUI basic scheme.

Theme

A theme is a cohesive collection of design tokens that defines the overall aesthetics of your design system. It encapsulates color schemes, typography styles, spacings, shadows, and more. The theme ensures uniformity across your design system, which in turn promotes a consistent user experience.

What’s truly powerful about themes in a composable design system is their extendability. You might have a base theme that sets some standard styling and theming for your components. However, to cater to specific needs, these base themes can be extended or overridden. This means you can adhere to the organization’s basic standards while also tailoring elements to suit the unique requirements of a specific team or project.

For example, the following ‘dark-theme’ extends the ‘base-them’:

A ‘dark-theme’ Bit component extends the ‘base-theme’ Bit component

For more on composable theming, watch this tutorial:

UI Components

UI components are the building blocks of your application’s user interface. They are reusable, self-contained pieces of UI such as buttons, cards, dropdowns, or navigation bars. These components utilize design tokens and themes to maintain visual and functional consistency.

In a composable design system, these UI components can be used, customized, and extended independently. It’s like having a toolbox filled with versatile tools, each having a specific function but also adaptable to various contexts. This promotes code reusability, speeds up development, and enhances the overall maintainability of your system.

With Bit, managing these elements of a design system becomes seamless and efficient. Bit offers the flexibility to pick and choose components, avoid redundant updates, and extend and customize UI components, design tokens, and themes to suit your project’s unique needs. This is the essence of a truly composable design system.

An ‘app-bar’ Bit component composed out of Bit components from the ‘design’ Bit scope

Getting started with your design system

Here’s how you can start using the components in your design system. This step-by-step guide will walk you through installing Bit, creating a new workspace, forking components, and exporting them to your scope.

Step 1: Install Bit

Begin by installing Bit on your machine with the following command in your terminal:

npx @teambit/bvm install

This command downloads and installs Bit via Bit Version Manager (BVM).

Step 2: Create a new workspace

Now, you need to create a new workspace for your components. You can do this using the ‘init’ command:

bit init

This command will initialize a new Bit workspace in your current directory.

Step 3: Fork a component

With your workspace ready, you can start using components from the design system. Let’s start with the ‘theme-provider’ component. To use it in your project, you’ll need to fork it into your workspace:

bit fork showoff.design/theme/theme-provider

The ‘fork’ command creates a copy of the ‘theme-provider’ component in your workspace, allowing you to modify it according to your needs.

In addition to the theme, you also want to use a UI component. For instance, let’s fork the ‘button’ component:

bit fork showoff.design/inputs/button

Forking these components provides the freedom to modify them according to your specific needs. The UI component we chose, ‘button’, is a great example. This component is wrapped around Material-UI’s button. The advantage of wrapping is multi-fold.

First, we can customize this component extensively, setting default properties such as color, and adjusting the API to our needs. Secondly, if we decide to switch the underlying library (Material-UI in this case) in the future, we can do so in one place without disrupting the components that depend on our button.

Step 4: Export to Your Scope

Once you’ve made the necessary changes to the component, it’s time to export it to your scope. This will make the component available for other teams or projects to use:

bit export

This command will export your modified component to your scope, making it ready for use across your projects.

That’s it! You’ve successfully installed Bit, created a new workspace, forked a component, and exported it to your scope. Now you’re ready to take full advantage of your composable design system!

Utilizing the design system

If you’re good with the design system components as they are and just want to start using them in your project, you can simply install them. This is an excellent option when you don’t need to customize or modify the components.

Let’s say you want to install the ‘theme’ and a ‘ui-component’, you can do so with the following commands:

npm install @showoff/design.theme
npm install @showoff/design.ui-component

This will install the specified components into your project, and you can start using them immediately.

It’s crucial to understand that while components are conveniently distributed as packages which can be installed in any environment, they are much more than that.

Each Bit Component is its own mini-repository, carrying its code, dependencies, and version history, among other things.

This unique structure enables each component to be developed, versioned, and managed independently while maintaining the ease of installation found with traditional package managers.

This balance between component independence and ease of integration is one of the key factors that make composable design systems so powerful and efficient.

Showcasing the Portfolio

To give you an example of how this works in a real project, let’s take a look at a portfolio website. This website utilizes various components from the design system, installed as mentioned above.

The portfolio website showcases various UI components and the theme, reflecting a unified and cohesive design language.

For a deeper look into the components used and how they’re organized, check out the scope where all the components are tracked and versioned using Bit. This will give you a clear understanding of how the design system is used in a real project.

Remember, installing components directly saves you from the overhead of maintaining them, as updates are handled directly by the design system. This allows you to focus more on building your project.

Extending the design system

Here, Bit steps in to fill the void. Bit allows for independent versioning of every component you create. In layman’s terms, if you’ve incorporated a Button component from your company’s design system and a newer version is rolled out by the Design team, you can effortlessly update your component to utilize this latest version.

This means you’re free from the burden of updating the entire design system package — something Bit eliminates the need for. You can think of each component as a standalone “project” or a “repository” for easy comprehension. Moreover, if you need to modify the Button component, you can do so within your workspace, and conveniently suggest alterations to the design team using lanes.

This blog post will delve deeper into these facets and more. To illustrate the concepts mentioned, I’ve chosen the showoff.design as the example scope. The Showoff organization is dedicated to aiding developers in showcasing their prowess. They provide a composable portfolio app tailor-made for developers. The app’s foundation is the design scope, constructed entirely from MUI components, all managed by Bit. So, let’s dive right in!

An overview of the design system

Composability is central to our approach in our quest to build a design system. Components are grouped into logical units called scopes, which enhance discoverability and comprehension.

The organization we’re looking at comprises various MUI components grouped into scopes, each offering unique functionality. For instance, some scopes contain base components without visual representations, while others add visual layers to these base components. By composing components in this manner, we can isolate potential issues, making them easier to handle.

The showoff.dev scope contains components such as the envs. A development environment (‘env’) component is a collection of tools and configurations a component uses for its development, build, and delivery. In simple words it means that your development enviorment is standarinsized and is always “attached” to the component you set it with. For example, the Card component is using the React With Mui env which sets the @mui/material as a peer dependency while wrapping the preview with the theme provider (and a whole bunch of other stuff).

But let’s get back to the design scope. In the picture below, you can see how components are divided into namespaces, making it easier to search the scope for the component you need. Moreover, you can see a visual preview of the component that helps you understand if it fits your needs.

The components are obviously using the fragments from the MUI library and giving them the twist that fits our org. For example, the Chip component is as simple as this:

import React from 'react';
import { Chip as MuiChip } from '@mui/material';

export type ChipProps = {
/**
* The content of the chip.
*/
content: string
};

export function Chip({ content }: ChipProps) {
return (
<MuiChip
label={content}
size="medium"
sx={{
"& .MuiChip-root": {
borderRadius: '6px'
},
"& .MuiChip-label": {
px: 0,
py: '8px'
}
}}
/>
);

Some components are composed of several ‘Bit MUI’ components. Have a look at the Header component:

import React, { ReactNode } from 'react';
import { AppBar, Container, Box, Toolbar } from '@mui/material';
import { Typography } from '@showoff/design.typography.typography';
import { Link } from '@showoff/design.navigation.link';
import { HeaderLink } from './header.type';

export type HeaderProps = {
logo: ReactNode;
navLinks: HeaderLink[];
themeToggle?: ReactNode;
};

export function Header({ logo, navLinks = [], themeToggle }: HeaderProps) {
...rest of the code

As you can see, it uses the navigation/link and typography components. The next section will discuss the benefit of composing Bit components together.

Independent versioning and auto-tagging

A crucial feature that Bit offers in building this design system is the independent versioning of components. This means every component you create can be updated separately without causing disruptions to the entire system. By using the command bit tag, you can tag versions of each component, a process akin to committing in git.

Unlike git commits, however, where the entire repository's state is saved, Bit's tagging system locks in the state of each component independently. It allows components to be marked as exportable and keeps track of all their changes.

If we continue the example from the previous section, every time we change either the link or the typography components, an update will be triggered for the header component as well, and a new version of it will be released.

This ripple effect is a cross-scope effect. The navigation/appbar component from the personal-portfolio scope is (also) composed from the header. Now if we change the link or the typography, it will Ripple to both the header and the appbar:

Every so often, during the quiet solitude of the night, my mind starts to wander. As I lay there, cocooned in tranquility, I start dreaming of a utopian software world. A place where everything is composable, where every piece fits into the grand jigsaw puzzle with perfection, where monolithic systems are relics of the past. I can’t help but chuckle at the sheer beauty of it. Ah, the joys of a developer’s midnight musings! One can only hope that such glorious days aren’t too far off. Hold tight, folks, a composable future is just around the corner!

The impact of a small change can ripple to different scopes and even different organizations.

Theming and styling

Different teams, also referred to as scopes, contribute to the design system’s structure by adding their own styles and themes. The delicate dance between providing uniformity while allowing for customization is crucial.

This is where your theme provider component comes into play, allowing consumers to adjust properties to suit their requirements. The challenge here lies in balancing standardization and customization. The theme provider accepts a theme prop, enabling you to imbue it with your unique theme. But how do we ensure consumers of the theme provider adhere to your organization’s standards?

The solution is to create a new theme by extending the base theme component. This approach allows your theme to remain consistent with the organization’s fundamental standards, while still providing the flexibility to customize it according to your team’s needs. Here’s a practical example:

import { themeCreator } from '@showoff/design.theme.theme-creator';
import { alpha, Theme, ThemeOptions } from '@mui/material';
import { baseTheme } from '@showoff/design.theme.base-theme';

/**
* Function that returns a configured dark theme
* @param additionalConfigurations - Additional configurations to be applied to the theme
* @returns Configured dark theme
*/
export function darkTheme(additionalConfigurations?: ThemeOptions): Theme {
return themeCreator(
baseTheme(),
{
palette: {
mode: 'dark',
primary: {
main: '#000',
...rest of the theme

In this case, we’re using the base theme as our foundation, overwriting properties as needed. What’s great about this is that every time the baseTheme is updated, your extended theme inherits these changes. So, if your organization decides to modify the primary color, your theme won’t be left in the dust. Instead, it will seamlessly adopt the change.

Distributing Components (Exporting/Publishing)

Bit streamlines the process of distributing or publishing your components. With a simple tagging action, your component is ready to be shared across other projects and developers.

The unique feature of Bit’s approach lies in its component-based architecture, which allows individual components to be distributed separately.

You no longer need to publish an entire design system package, which is typical of monolithic projects. This method enhances collaboration and usability across different organizational projects.

When a component gets tagged, Bit triggers a series of tasks including linting, testing, building, and packaging the component. These steps ensure each piece of software is performing as expected. And remember, you have full control over how this process looks because it’s determined by your environment component.

Every new version you release sets Ripple CI into motion. It ensures updates ripple through all dependent components. Consider this scenario: Oded Reuveny, my colleague and good friend, updates the navbar component. This single update causes a ripple effect reaching all dependent components and eventually five different apps. The updates are deployed seamlessly to their respective host services, showcasing the power and efficiency of Ripple CI.

Component adoption and usage

Bit aims to simplify the process of adopting and using components. Through organized grouping into scopes and the feature of independent versioning, Bit provides a clear and structured environment. This facilitates developers in finding, understanding, and incorporating components into their projects, encouraging wide adoption across numerous organizational projects.

Incorporating a component as a dependency involves installing it in your Workspace:

bit install @showoff/design.icons.bit

After installation, the component is immediately available for use in your project. When you use this component in one of your own, Bit automatically includes it into the dependency graph. (See image below)

Updating

Bit provides an efficient way to manage component updates. Developers can effortlessly modify their components to include new changes or improvements. The feature that allows each component to be independently versioned makes updates more manageable, as they can be conducted on a component-by-component basis, eliminating the need for a full design system update. This enhances the flexibility and manageability of the system.

As highlighted in the preceding section, Ripple CI can be used to automatically update your components. However, if you prefer manual updates, you can run the command bit update. This command checks for newer versions of your components and prompts you if you want to update them. Additionally, it checks for updates for installed packages within your components, keeping your projects current and efficient.

Collaboration and suggestion of changes (“Lanes”)

Bit significantly boosts teamwork by facilitating the suggestion of changes. When a developer needs to modify a component, they can do so within their workspace and propose changes to the design team. This cultivates a collaborative ecosystem where system improvements and optimizations are a continuous process.

Consider this scenario: you’re using the ‘inputs/button’ component from the design scope, but you need the button to have extended API capabilities that aren’t currently included. Here’s how you handle this:

  1. Import the component into your workspace with bit import showoff.design/inputs/button.
  2. Create a new lane with bit lane create extend-button-api.
  3. Modify the source code of the component to reflect your changes.
  4. Snap your changes with bit snap -m "extend the button api to support xyz".

After you’ve exported the lane and Ripple CI has built it, you can generate a Change Request (CR) and forward it to the Button’s maintainer. They can then review and approve the release of the new version, streamlining the collaborative process.

Conclusion

In summary, Bit offers a comprehensive solution to manage the lifecycle of design system components, promoting a more effective, efficient, and collaborative working environment. From distributing components individually, like giving out keys to unlock specific functions, to easing their adoption and usage via intuitive grouping and independent versioning.

Bit also simplifies the update process, enabling developers to effortlessly integrate changes and improvements on a per-component basis, rather than updating the entire design system. This flexibility makes the system more manageable and adaptable to changes.

Moreover, Bit fosters collaboration by allowing for easy suggestions of changes, where developers can modify a component within their workspace and propose alterations to the design team. This facilitates an ongoing cycle of system enhancements and optimizations.

With Bit, you’re not just managing components; you’re managing a vibrant, evolving ecosystem that drives your software development forward with efficiency, flexibility, and collaboration. The future of composable software is here, and it’s more accessible than ever.

--

--

Making 𝘾𝙤𝙢𝙥𝙤𝙣𝙚𝙣𝙩-𝘿𝙧𝙞𝙫𝙚𝙣 Software a 𝐑𝐞𝐚𝐥𝐢𝐭𝐲 @ 𝐛𝐢𝐭.𝐝𝐞𝐯