arrow_back All posts
Figuring out a design system setup

Figuring out a design system setup

How to effectively align the user interface across multiple projects? Let's figure out a better way to build a shared design system setup!

Introduction

As companies grow and scale, maintaining a unified UI across multiple projects becomes increasingly challenging. Many teams struggle with duplicated components, inconsistent designs, and redundant code across different applications.

To help solve these issues, some teams introduce design systems — a set of rules, guidelines, and sharable components that help unify the UI across projects. It might sound simple, but planning and developing a design system is a complicated task, as it depends heavily on the company's design processes, which might vary widely even from project to project.

What to look out for?

The design system can be used to build any number of applications within an organizationThe design system can be used to build any number of applications within an organization

Even though there is no "one size fits all" solution, there are some useful pointers to keep in mind, regardless of the company or the specifics of the project you are currently working on.

👉 Detach applications from libraries

Having a well-defined architecture for a complex project is one key to ensuring the code does not become a bunch of spaghetti code. One effective way to maintain a clear hierarchical order is to split the projects into multiple independent packages.

The code that is shared (ex., design system) is extracted into a separate library, whereas the consumer (ex., landing page) remains a separate application. The library becomes the direct dependency of the application and is specified in the package.json:

// landing-page/package.json

{
  "name": "landing-page",
  "dependencies": {
    "design-system": "1.0.0"
  }
}
// landing-page/index.tsx

import { Button } from 'design-system';

const LandingPage = () => <Button>Buy now!</Button>;

export default LandingPage;

By organizing your projects and defining strict boundaries between libraries and applications, you can get some really great benefits:

  • Modularity — both libraries and applications are independent packages that can be configured in different ways depending on the specific needs.
  • Maintainability — adding changes becomes easier and more predictable.
  • Scalability — adding, replacing, and removing libraries or applications is straightforward due to the hierarchical relationship.
👉 Find the source of truth

The design system can be the source of truth for both designers and developersThe design system can be the source of truth for both designers and developers

One of the ways teams can build stuff faster, is if all shared components can be found in one place — in the design system. They are defined in the code, versioned, and accessible to both developers and designers.

If done correctly, the design system ensures that all teams use the same components, guidelines, and rules. This helps reduce friction between designers and developers and significantly decreases the overhead of aligning across multiple projects.

👉 Add flexibility where needed

As projects grow, so do the requirements. When receiving a complex task for a feature that includes some crazy designs, it might be tempting to respond: "If it is not in the design system, I am not implementing that!”. Sure, the design system might be the single source of truth, and those guidelines should be followed. On the other hand, the source of truth can be best compared to a living organism - it mutates, adapts, and changes.

A design system should allow for some flexibility, for example, by providing the ability to compose variants or pass custom themes to components. Consider a <Button/> component in your shared design system. This component can be customized with different themes in various consumer applications.

// a-design-system/Button.jsx

import React from 'react';

const Button = ({ theme }) => (
  <button style={{ backgroundColor: theme.primaryColor }}>Click me!</button>
);

export default Button;

Each application can provide its own theme:

// landing-page/theme.js

export const theme = {
  primaryColor: 'blue',
};
// admin-interface/theme.js

export const theme = {
  primaryColor: 'red',
};

By allowing for flexibility in the design system, we can help the developers and designers collaborate better and build things much faster. 🔨

Visualize it!

Storybook: Frontend workshop for UI developmentStorybook: Frontend workshop for UI development

Having the design system as a single source of truth defined in the code is great. But how can w give it life and transform it into a living and breathing organism?

Storybook might be a perfect tool to visualize design systems:

  • Isolated — implemented as a separate application, it can exist in parallel with your other projects.
  • Intuitive — listing and documenting design system stuff like UI components, guidelines and rules is really easy and straightforward.
  • Flexible — the addon features can help improve the development process with features like theming, viewport resizing, and more.

The design system can be visualized using StorybookThe design system can be visualized using Storybook

By connecting a design system with a presentational tool like Storybook, teams get access to an independent instance of the design system that is always available to be inspected and played around with. This instance can even be shared internally within the organization or displayed publicly to the rest of the world! 🎉

Setting up the library

The design system can be build as a compilable libraryThe design system can be build as a compilable library

If a design system is structured as a separate library, it should be able to handle its own development processes. One of the most common ways of developing a design system is extracting it into a separate library, configuring the separate development processes, and finally setting up versioning and packaging.

Developing the design system as a standalone library brings several benefits:

  • Independent workflows — having independent linting, testing, and compilation workflows ensures the design system library is self-contained and does not depend on other applications.
  • Separate dependencies — having a separate package.json for the library, you keep full control over the direct dependencies and the supported versions.
  • Optimizations — when compiling and packaging the library, you can manually configure and change the output bundle specifics to match your needs.

What about monorepos?

The approach above works well when projects are separated into multiple repositories. But what if your applications are built within a monorepo?

During last couple of years, I have been trying to figure out how to effectively share UI components across multiple projects that are part of the same repository. One of the best ways I found was introducing a shared design system library within the monorepo using React, Storybook, PNPM, and Nx.

Create the Nx workspace:

$ npx create-nx-workspace my-workspace

Add a shared library "ui":

$ npx nx g @nrwl/react:lib design-system

Add the Storybook interface:

$ npx nx g @nrwl/react:storybook-configuration --name=design-system

This setup contains all the benefits of having separate applications and libraries while remaining in the same repository.

Conclusion

Building a shared design system library is not an easy task. But, if equipped with the proper tools and processes, any team can succeed at creating a well-functioning setup.

NxPNPMReactStorybook