"To be authentic is to be at peace with your imperfections" by Simon Sinek.
Before we dive in, let's acknowledge the positive aspect: having a cross-group library that's been adopted by our products and provides a unified user experience is a significant achievement. We're committed to continuous improvement, aiming for streamlined processes, clear architecture, and robust build and deployment strategies.
It's important to note that the critiques and lessons shared in this article in no way diminish the hard work, dedication, and ongoing investment we're all making in this library.
Two years ago, we realized our organization lacked a unified design system to be used across our suite of applications. This realization wasn't shocking; we were transitioning from isolated, independent applications to a more unified user experience. In this new paradigm, users could interact with multiple applications seamlessly, whether through iframes or dynamic content loading.
While we achieved the primary goals set by the product team for the design system, this article isn't about patting ourselves on the back. In truth, our short-term planning led to many challenges. We faced issues related to adaptability, maintenance, versioning, and more. Additionally, the lack of clear ownership and the diverse needs of multiple products led to managerial headaches.
So, this article is about humility and learning. I'll candidly share the pitfalls we encountered, because there's as much—if not more—to learn from mistakes as there is from successes.
Note: It's worth mentioning that leveraging an existing library like Material UI (MUI) was, and still is, the right choice for us. The decision to build on an existing library or start from scratch depends on your specific goals. If your design system library is a product, whether paid or open-source, you should create your own components from scratch. If not, it's smarter to use a library that's already made, making it work for what your applications need.
Ready to get started? Let's dive in.
In our group we have React experts who've built complex, high-performing applications. So, creating a design system library should be a walk in the park, right? Not quite. As we discovered, React expertise doesn't automatically equip you for the unique challenges of building a design system library.
One glaring example was our decision to closely follow the Material-UI (MUI) library's API and structure. With limited knowledge about design systems, we chose to emulate those who seemed to know best, aiming to benefit from MUI's accessibility support, component offerings, and other features. We'll dive deeper into this issue in the next section.
Another pitfall was our struggle with customization. When we did attempt to create components that adhered to our design requirements, we found that they were not easily customizable. This happened because our team's developers were not used to some React techniques that help build flexible design systems.
Writing a design system library isn't just another frontend task; it requires specialized knowledge and skills. If your team doesn't already possess this expertise, consider forming a dedicated squad for the initial version. This squad should consist of strong, skilled frontend developers led by someone with an architecture-oriented approach.
Creating a design system library is not a side project. Make sure to allocate sufficient time and resources to this endeavor. Clearly communicate to all stakeholders that this is a significant commitment and not something to be squeezed in "along the way."
Popular design systems like Material-UI (MUI), Chakra-UI, and others are built to be robust and versatile. They offer a wide range of overlapping APIs to cater to a various of use-cases. While they may have their own opinions, for the sake of this article, let's consider them non-opinionated. They're designed to fit into almost any application architecture you throw at them.
However, when you're crafting a design system tailored for your own suite of applications, the goals shift dramatically. You're not building for the world; you're building for your specific ecosystem. This leads to a more opinionated approach, focusing on APIs that precisely meet the needs of your applications. For instance, you don't need five different ways to customize themes; one or two methods that align with your architecture should suffice.
But let's clear up a common misconception: being opinionated doesn't mean being strict. A strict API forces applications to work around the library, while an opinionated one promotes smoother integration by exposing just what's needed. This aligns well with the open-closed principle, allowing for easier future extensions.
At Kaltura, for example, our Professional Services department needs the ability to customize themes at multiple levels. While MUI provides various ways to achieve this, offering too many options can lead to an unsustainable, hard-to-support system. By controlling the API and tailoring it to specific needs, we create a win-win situation for everyone involved.
When building an in-house design system, focus on what your specific applications need. This allows you to create an opinionated API that promotes smoother integration and easier maintenance.
An opinionated API isn't a limitation; it's a focused toolset that serves your specific needs without forcing workarounds. It should be designed to be open for extension but closed for modification, following the open-closed principle.
Offering too many customization options can lead to an unsustainable system. Control the API to ensure that it aligns with your architecture and can be effectively maintained in the long term.
Let's make this section a bit more interesting by diving into an example. Imagine you have a button component in your design system with the following requirements:
- Can be disabled or in a loading state
- May have an icon on the right side
- Comes in three sizes
- Offers three variations (circle, pill, borderless)
Pause for a moment and think: how many properties should such a button expose?
To give you some perspective, I tried counting the properties of a MUI button and lost track after about 140. For a general-purpose library like MUI, this makes sense. They need to cater to every HTML attribute, accessibility feature, animation, and more.
When you make a library for just what you need, things are different. You might only need about 7 properties for your product. If you add some basic technical parts like 'ref', 'styles', and 'classes', you get to around 14 properties total. But as we inherit from the MUI button, we end up showing many more properties than our designers asked for.
Now, let's consider a more complex component like a dialog which also has compositions of inner parts like header, body, footer, etc. For our tailored library, we should have offered just two variations of dialog with about 10-15 properties. But by exposing the MUI's dialog component and making changes to it, we accidentally shared too many properties and also allowing compositions that are not part of the company design system. This made our dialog box too complex and harder to manage than we intended.
For our tailored library, we needed to expose just two compositions and about 10-15 properties. But as we exposed the with customizations, we exposed a much larger, potentially hard to handle component API.
When tailoring an API to your specific needs, you often require far fewer properties than a general-purpose library would offer. This makes your library easier to understand, use, and maintain.
Understanding both your product and technical requirements is crucial. This allows you to expose only the properties that are genuinely needed, avoiding unnecessary complexity.
Inheriting all properties from a general-purpose library can lead to a bloated, hard-to-maintain API. Moreover, you risk exposing properties that not only are irrelevant to your product requirements but may also directly conflict with them, making it impossible to meet those requirements effectively.
As components become more complex, the number of properties can grow exponentially if you're not careful. Tailoring your API keeps this complexity in check, making your library more sustainable in the long run.
In today's development landscape, it's common to equate having a Storybook with having a documentation site. While Storybook is an excellent tool for showcasing components and their various states, it's not a one-to-one substitute for comprehensive documentation.
Storybook excels at visual representation and interaction, allowing you to see components in isolation and play around with their properties. However, it often falls short in providing the context, best practices, and architectural guidelines that are crucial for a design system. It doesn't explain the "why" behind certain design decisions or how components should be used together with each other to build cohesive user experiences.
Simply put, Storybook helps developers see how a button works by clicking on it. But it doesn't guide them to write good explanations about when to use this button or how it fits into the bigger picture of the design. Since Storybook focuses more on making components than explaining them, it can lead to weaker documentation.
While tools like Storybook are valuable for component visualization, they are not a replacement for thorough documentation that includes context, guidelines, and best practices.
A design system is more than a collection of components; it's a set of rules and philosophies that guide how those components are used. Storybook won't provide this context, so make sure your documentation does.
A well-crafted documentation site serves as an invaluable resource for both developers and designers. For developers, it's a guide to understanding how to implement and extend components. For designers, it's a reference point to validate that the implementation aligns with design requirements. To foster effective collaboration and feedback, your documentation should comprehensively cover all possible states of a component, including states that are changing quickly like loading, busy, and error.
Storybook offers two modes: Canvas and Docs. While Canvas provides a hands-on, interactive way to explore components, it's often more convenient for developers and not ideal for serving as a documentation site. In contrast, the Docs mode is where the true power of Storybook as a documentation tool comes into play. It allows for more comprehensive coverage and should be the focus if you're aiming to create a robust documentation site. Configuring for a "docs-only" build, although challenging, is a crucial step in achieving this goal.
When our design system library was first initiated by one of our developers, we didn't invest much time in framing the concepts, motivations, and assumptions behind it. We naturally chose to use MUI, but we didn't stop to think about possible problems or other options that might have been better.
MUI is an excellent library and serves our applications well. However, when you're developing a library, dependencies take on even greater importance. Unlike an application, where you own the page, a library is more like an invited guest. Everything from the HTML it generates to the classes it exposes and its bundle size becomes crucial.
Moreover, the library sets the tone for how application developers approach UI. For example, if MUI has a particular way of managing dynamic styles, our library will echo that approach. To illustrate, let me share one positive and one negative example from our experience. On the positive side, about a year after integrating the library into our applications, we received a critical request to support Content Security Policy (CSP). Fortunately, since MUI supports it, our library does too, saving us from having to start a new design system library from scratch.
On the flip side, a design system library typically comes with a theme, which is conveniently exposed to the application. In our case, we started echoing MUI's theme, and it became part of the public API of our library. As I'll discuss in the next section, the MUI theme doesn't align with our library's theme. Because it was there from day one, we've been working extremely hard to deprecate it—an effort that is far from over.
In addition to the theme challenges, another aspect of Material-UI (MUI) that we encountered involves its handling of DOM classes. MUI is designed to expose classes on the DOM, allowing for advanced customization of component. This feature is particularly beneficial for professional services and other developers who need to tailor the look and feel of components to specific requirements at runtime.
This approach, however, has made our design system heavily rely on MUI's specific methods. By integrating MUI's DOM classes into the public interface of our components, we've created a strong link with MUI. This connection has a significant implication: as we are committed to not breaking the customizations made by professional services, it effectively means we cannot upgrade MUI without risking issues. Upgrading MUI could lead to unexpected behavior or problems in our components due to changes in how MUI's classes are structured or function.
We are now working on separating our design system from MUI's DOM classes. This means finding new ways to let users customize components that don't rely so much on MUI's classes. This is an important goal for us as we improve our design system."
Dependencies aren't just code; they're a set of decisions and limitations that you're importing into your project. Whether you bundle a library as a dependency or count on the host to provide it as a peer dependency, you need to be mindful of what you're introducing.
When you're a guest in someone else's application, every byte matters. Be conscious of the footprint your library leaves, from bundle size to the HTML and classes it generates.
The decisions you make in your library will influence how application developers approach their work. Choose wisely, as these choices can have long-term impacts, both good and bad.
Sometimes the implications of a tech stack choice aren't immediately apparent. Always consider how well a dependency will align with potential future requirements, as we discovered with our need to support CSP.
The heart of a design system lies in its theme, which is fundamentally based on design tokens. These tokens are the basic building blocks of your app's design, just like a language for your designers. Changing a single design token can significantly impact all the components and layouts that use it. Moreover, users can switch themes at runtime to alter the overall experience—think light mode to dark mode.
Since this is your company's library, it should communicate using your designers' tokens. It's crucial to expose only the essential set of tokens that your designers use in tools like Figma. Otherwise, you'll find yourself unable to align the application with their designs after the initial setup.
Remember, the design tokens and the classes you show become official parts of your system, even if you didn't intend it. This means you'll have to support both the underlying library's tokens and your designers' tokens, which will lead to conflicts.
Before releasing a component, carefully review the injected DOM HTML and make informed decisions about the classes it includes. Assume that your company's professional services team—or the users of your library—will find and use these classes to meet their deadlines and design goals.
Any design token you expose that isn't defined by your designers is a ticking time bomb for future conflicts. Designers use tokens in their tools like Figma and expect changes to reflect in the final product. Conflicts with underlying design system tokens will prevent you from achieving the designers' vision.
Your theme, your design token. The only people who should influence the theme are your designers—not even you. Unlike component APIs, where you're expected to add essential or integrative props like
ref, and others, the theme is the designers' domain. Respect that and act as their guardian.
For context, check out the default theme of MUI. It has 1,372 lines when formatted, compared to our company theme's mere 119 lines. Imagine the number of unnecessary tokens you're adding by exposing the underlying theme tokens.
We've covered a lot of ground in this article, from the challenges of building a design system library to the nuances of API design, documentation, and theming. The journey has been enlightening, to say the least, and we've learned valuable lessons along the way.
Cover image crafted with Picsart.