"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.
From React Expertise to Design System Novice: Why Knowing React Isn't Enough
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.
Lessons Learned
Build a Specialized Squad
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.
Allocate Time and Resources
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."
Identity Crisis: The Conflict Between General-Purpose and Opinionated Design Systems
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.
Lessons Learned
Tailor to Your Ecosystem
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.
Opinionated Doesn't Mean Strict
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.
Control the API for Sustainability
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.
API Design Decisions: The Risks of Inheriting vs. Tailoring
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:
- Clickable
- 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.
Lessons Learned
Less Can Be More
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.
Know Your Requirements
Understanding both your product and technical requirements is crucial. This allows you to expose only the properties that are genuinely needed, avoiding unnecessary complexity.
Be Cautious When Inheriting
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.
Complexity Scales
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.
The Illusion of Documentation: Why Storybook Isn't a Substitute
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.
Lessons Learned
Don't Confuse Tools with 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.
Context Matters
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.
Documentation: A Dual-Purpose Resource
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's Hidden Power: Why Docs Mode Matters
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.
Use-Case Blind Spots: Why Our Tech Stack Choices Were Wrong for a Library
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."
Lessons Learned
Know What You Bring to the Table
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.
Consider the Footprint
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.
Your Choices Echo
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.
Evaluate Long-Term Compatibility
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.
Design Tokens and Theme: Keep Them Separate from the Underlying Library Tokens
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.
Lessons Learned
Always Look Through the Lens of Professional Services
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.
Always Look Through the Lens of Designers
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.
Maintain Your Own Theme
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 onClick
, classes
, children
, 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.
Summary and What's Next
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.
Top comments (3)
Great post, Eran! This is a really helpful guide.
On the note of your prologue, I hope you and your loved ones stay safe. I've been watching the events unfold from here in the US and am really concerned for all those innocent folks in danger. Wishing safety and comfort to you and those around you!
🇮🇱🇺🇦✊
With all my heart, I support Israel, wishing for your sky to become peaceful as soon as possible, and for all captives to return home!
Thank you for supporting Ukraine! I appreciate all your significant support all this time.
And this is very important prologue. It is crucial for people around the world not to forget that there is a war in our homes!🇮🇱 🇺🇦