Harness design systems with styled-components and React. Craft efficient UIs using Storybook. Elevate React components for a seamless web development experience.
Your own components library
I recall my time working for a company with a distinctive brand identity. During this tenure, a consistent look and feel were essential across all our applications, as emphasized by our project manager. The challenge we faced was the presence of significant duplication in presentation code across our four applications, primarily because individual teams had developed them separately.
In light of this, the company was gearing up to create additional applications, prompting the pressing question:
"How do we efficiently reuse this duplicated code while maintaining a uniform appearance across distinct applications?"
The solution lies in the establishment of a components library, a task that can prove to be quite formidable. This is because numerous factors must be carefully considered during the implementation of such a library. Questions arise, such as where to store it, what stack is most suitable, and what should serve as the definitive source for documenting these components, among a multitude of other critical considerations.
This is precisely why I've crafted this article. I aim to guide you through the process of effectively documenting your design system, provide insights into where to store it, offer advice on selecting the right tech stack, and walk you through the steps of constructing a components library, all with the intent of making this topic more accessible and comprehensible to you.
Below, you have the final result that you'll have at the end of this long article:
The demo of full React components library
What is a design system?
A design system is a group of rules, restrictions, color palettes, spacings, and general workflow rules when designing/building a UI.
Why are these rules necessary, you may wonder? The answer is straightforward:
"They are vital for maintaining a consistent and cohesive user interface across a multitude of solutions."
The development of every component library should have its foundation in a robust design system. Without it, you risk ending up with a disorderly, unattractive, and visually unreliable presentation layer.
Before you embark on coding, it's essential to establish the framework of your design system. This foundational structure can be housed within tools like Figma or AdobeXD. For particularly intricate design systems, you might consider supplementing your documentation with platforms such as Confluence or other suitable alternatives.
Here you have the most popular design systems that are well described:
Material design - https://m3.material.io/
Apple design - https://developer.apple.com/design/
Fluent UI - https://fluent2.microsoft.design/
Exploring design system tokens
When I discussed the design system, I introduced the concept of individual decisions that designers make to ensure a consistent look and feel. These decisions are referred to as design tokens.
They encompass a wide range of elements, such as an available color palette, spacing values (e.g., 4px, 8px, and so on), font variations, and numerous other constants that will be consistently applied to every component.
Now, take a moment to examine the GIF provided below. Within it, you'll find a multitude of design tokens that play a pivotal role in defining our visual identity and aesthetic.
The base of design system
Mapping design tokens to code
Now, it's time to map the designer's decisions into code. Here, I'll provide you with examples of how this process is carried out in both the Tailwind and Material UI libraries.
The choice of your tech stack is not the determining factor. When translating design tokens into code, the crucial aspect is to ensure that they align with the specifications outlined in Figma or Confluence. When designers decide to modify these tokens, it's imperative to synchronize these changes within your codebase.
Furthermore, it's essential to incorporate the flexibility of overriding these decisions if a specific application necessitates such customization.
Additionally, providing a straightforward method for applications to effortlessly integrate and utilize these tokens is a key consideration.
Tech stack and components lib
Selecting the "right" tech stack for your components library involves a straightforward process.
1. Gather requirements: begin by collecting and comprehensively understanding the requirements.
2. Prioritize Goals: prioritize your objectives, ensuring that they align with the business's needs and desires.
3. Proof of Concept (PoC) ✅: develop a small PoC, incorporating all the prioritized goals as ✅, to validate the feasibility and functionality.
4. Risk Assessment ⚠️: identify potential risks that may arise during development and have strategies in place to mitigate them.
5. Limitations ❌: recognize any limitations or constraints in the chosen tech stack and be prepared to work within these boundaries.
6. Build the Library: once the above steps are complete, proceed to build the actual components library.
Choosing tech stack
Imagine you have four applications, developed using NextJS and plain SPA React. These applications are equipped with dark theme support and consistently achieve impressive performance ratings of 90+ in the Lighthouse tool's performance tab. Presently, they implement styles using SCSS syntax, a simplification of the broader details to keep this article concise and manageable.
One initial consideration is the creation of a components library using Tailwind, which seems like a promising approach. However, a significant roadblock arises: the existing styles in your applications are structured in separate files.
Transitioning to Tailwind would necessitate a massive overhaul of the codebase, presenting substantial risks to the business. Given these challenges, it appears that adopting Tailwind is not a viable option.
A more favorable option to consider is implementing the styled-components library. This choice offers the convenience of transferring existing styles from the .scss files by simply copying and pasting them. Developers would primarily need to add a wrapper for each component, significantly reducing the amount of work compared to transitioning to Tailwind.
Moreover, styled-components enables the encapsulation of styles, diminishing the risk of global class collisions. This option appears to strike a good balance between compatibility with the current codebase and risk management.
Indeed, while there are other alternatives, styled-components appears to be a strong and practical choice. It offers seamless theming options, enjoys widespread support, and is highly popular within the development community. Although it may result in a slight performance decrease of approximately 10% compared to Tailwind in Lighthouse, the associated risks are relatively low. This performance difference can be mitigated by adopting an atomic-css approach similar to Tailwind. The minimal configuration required for NextJS can be addressed by creating a small PoC, making it a manageable solution for your specific needs.
It's a sound decision to opt for React + styled-components as the foundation for building your React components library. This choice aligns well with your requirements, and the associated risks are indeed quite manageable, making it a suitable and practical solution.
You've hit the nail on the head - there's no such thing as a perfect tech stack. It's always a matter of weighing the pros and cons to find the right fit that aligns with your specific requirements and business objectives.
What we build?
We're going to create the FigaUI library, tailored to meet our specific needs. This library will empower us to craft both straightforward and sophisticated components, providing the flexibility to share design tokens, customize the appearance, and maintain a scalable structure.
Starting from folders/files structure
There's no one-size-fits-all rule for this. Developers use various approaches like Atomic Design or their own conventions. I'll go with my convention, but the most important thing is to be consistent when building components. The structure may change over time, but I usually use something like this:
Alright, you might be wondering why I'm developing four distinct libraries to implement a single design system. Well, there are several compelling reasons for this approach:
- if someone wants to take the core of my design system and implement their own, they can simply load the "figa-ui-core" library, which will be lightweight,
- splitting extensive design systems into distinct libraries enhances my CI/CD process. It allows me to run tests separately, making it easier to identify where issues may arise in the layers,
- in a team of developers, dividing the work becomes more straightforward, and it helps minimize conflicts,
- if someone only needs the fundamental components of the design system, such as styles, links, buttons, and inputs, and wants to avoid the more complex components like calendars, grids, and tables, they can achieve this by simply importing "figa-ui-base" lib,
- breaking down the design system setup into separate libraries decreases the coupling between component code and configuration setup,
- having a designated facade for each layer simplifies migration to other technologies. For example, if "styled-components" were to become deprecated, you could easily replace it in "figa-ui-base" while ensuring that it fulfills the established contract,
- if someone wishes to replace a particular layer, they won't disrupt the others. They just need to ensure that the contract between libraries is upheld,
- this approach offers a significantly improved developer experience, with faster tests, the ability to work in isolation, and the option to open a single workspace, all while avoiding IDE lags,
Structuring the internal code of the libraries will vary slightly depending on the library's purpose. In the case of figa-ui-core, the structure will encompass essential utilities, each residing in a separate file, with shared type definitions. These will all be re-exported for easy access.
The figa-ui-core library structure
For figa-ui-base and figa-ui-components, scalability is crucial since there will be numerous files containing components, tests, models, and Storybook definitions. Placing them all within a single directory would result in a disorganized structure. To address this, we will create a separate directory for each component, following a consistent convention.
The figa-ui-base library structure
The figa-ui-components library structure
The structure for figa-ui-design-system will involve distinct type definitions and re-exports consolidated in a single index.ts file. Moreover, it will incorporate a "wrapping" logic to facilitate the sharing of the design system across multiple components.
The figa-ui-design-system library structure
Implementing "figa-ui-core" library
We'll begin by creating type definitions that describe our design system, along with supplying a subset of design tokens to support this effort.
The use of numbers like 50, 100, etc., serves to encourage developers to override the default theme for changes. The 50-point difference between values allows for easy insertion of new values without affecting the existing ones.
In this library, we'll incorporate key features essential for building any design system in accordance with the previously defined contracts.
Implementing "figa-ui-design-system" library
First, install npm i styled-components.
I'll merge class names in components to avoid manual conditional checks, and I'll include the classnames library for this purpose. To use it, install npm i classnames.
This library should contain logic for sharing and modifying the design system setup, using the React Context API.
As you've seen, we've introduced a straightforward object aligning with the defined interface in figa-ui-core library, encompassing colors and spacing options. Now, we'll create a provider responsible for taking this initial setup and allowing for initial overrides or modifications through dedicated functions for each wrapper component.
We've provided a user-friendly hook with wrapper existence checks. If someone forgets to use DesignSystemProvider, an error message will point to the problem's root. The internal Context API remains hidden, with useDesignSystem serving as the main point of access.
Take note of the import of StyledThemeProvider, which injects value.setup into the theme property. This configuration permits the utilization of our theme values within style definitions.
Using our design system setup is straightforward. Refer to the example below, where we wrap our components and employ the hook to read and update the design system.
Implementing "figa-ui-base" library
This library focuses on core style setup and offers a set of frequently used components for starting applications. To begin, we'll customize styled-components type definitions to provide hints during style development using our theme structure. We'll start with a simple defs.ts file.
Now, we'll create a style setup using the styled-components API, making use of our design system setup.
It's time to create the first component. It will be Font.
Before using our components in the application, we must integrate these styles into the global setup. In the base library, we want to provide the flexibility to use our design system through components or classes, ensuring both options work seamlessly. This entails importing and including font component styles.
In the final step, import and apply GlobalStyle in your application.
It's worth noting that we have the option to utilize either the CSS approach or components, providing a powerful and versatile split in our approach.
Implementing the "figa-ui-components"
This is the simplest place to work with! We'll include standalone components here that won't be part of the initial bundle, each with its individual style wrapper instead of global styles. Let's start with a basic imitation of a calendar widget.
Before you explore the demo...
We have organized these four separate directories for each design system component, which could potentially become individual libraries with their own package.json files.
However, for practicality, we've housed them within a monorepo. In the section below, you'll find an example of how this concept can be implemented in the linked repository, so feel free to explore this concept in more detail.
Full example on Codesandbox platform
The result and bonus
You can explore the rules in action in the provided Codesandbox example above. However, for a real-world design system implementation, I'm currently working on a monorepo located under the Dream stack for React developer repository.
The demo of full design system implementation
In this article, we've primarily addressed the "structural" and "architectural" aspects of building a robust design system. However, creating a comprehensive design system involves much more. If you're interested in delving deeper into this topic, I recommend reading the following articles:
Explore Storybook and Chromatic in ⭐ Chromatic and storybook article.
Explore scalable repository setup in 🌟 Dream stack for React dev article.
Conclusions and thoughts
You've now learned how to effectively integrate design system tokens into your codebase and share them across components. Additionally, you understand how to structure a comprehensive design system to mitigate scalability issues and extend your codebase with ease.
Remember that this is merely a proposal, and you can implement the same concepts in Tailwind, with an alternative structure, without TypeScript, and so on. The key is to maintain consistency and ensure you consume your design tokens within the created components.