Building a design system for your company is one of the most effective ways to ensure consistency across your products. In fact, most serious projects today have a design system in place. A well-structured design system not only streamlines design and development but also helps create a cohesive user experience.
In this article, we’ll walk through some best practices for structuring your company's design system to ensure it's maintainable, scalable, and it's not leaking external libraries APIs/concepts.
Before diving into best practices, let’s define what a design system is. Simply put, a design system is a collection of reusable components, governed by clear standards, that can be combined to build consistent user interfaces. It acts as a single source of truth, bringing together all the elements necessary to design, develop, and maintain a product.
This guide is particularly useful for teams building design systems that rely on a UI library, as the most common mistake is allowing external library APIs or concepts to bleed into the design system, creating unnecessary complexity and confusion.
Let's dive into the most important best practices that will help you structure a sustainable and effective design system.
What this means: Provide consumers with a single entry point for importing design system components, rather than having them reference multiple sources (for instance, importing from both your custom library and the underlying third-party library).
// ❌ Don't allow consumers to import components from multiple sourcesimport { TextField } from '@your-company/design-system';import { Button } from '@mui/material';// ✅ Restrict them to only import from your Design System's packageimport { Button, TextField } from '@your-company/design-system';
Benfits:
What this means: Create a custom theme by defining design tokens (e.g. colors, typography, spacing) as CSS variables. Instruct your consumers to apply these variables at the top of their app to ensure consistency. Most of the modern UI libraries nowadays allows you customize their theme using a ThemeProvider
, or similar API.
If you are building custom theme from scratch, you can define your theme using CSS variables globally like this:
:root {color-scheme: light dark;--color-primary: #1745c2;--color-danger: #c10000;--color-gray-100: rgba(1, 5, 20, 0.05);--color-gray-200: rgba(2, 5, 19, 0.08);--color-gray-300: rgba(2, 5, 18, 0.17);--color-gray-400: rgba(2, 5, 18, 0.38);--color-gray-500: rgba(2, 5, 17, 0.5);--color-gray-600: rgba(2, 5, 16, 0.67);--color-gray-700: rgba(3, 5, 15, 0.77);--color-gray-800: rgba(3, 6, 13, 0.85);--color-gray-900: rgba(3, 6, 13, 0.9);@media (prefers-color-scheme: dark) {--color-primary: rgb(95, 149, 255);--color-danger: rgb(255, 154, 134);--color-gray-100: rgba(38, 41, 47, 0.65);--color-gray-200: rgba(45, 48, 55, 0.8);--color-gray-300: rgba(55, 59, 65, 0.8);--color-gray-400: rgba(87, 91, 99, 0.8);--color-gray-500: rgba(135, 140, 150, 0.8);--color-gray-600: rgba(191, 196, 207, 0.8);--color-gray-700: rgba(222, 229, 241, 0.8);--color-gray-800: rgba(222, 229, 241, 0.8);--color-gray-800: rgba(227, 232, 242, 0.85);--color-gray-900: rgba(236, 238, 244, 0.9);}}
Or, if you are using a UI library, you can use the library's API for it. For example, using Material UI, it would look like this:
import { createTheme, ThemeProvider } from '@mui/material/styles';import { orage } from '@mui/material/colors';const theme = createTheme({cssVariables: true,/* other theme overrides */});function App() {return <ThemeProvider theme={theme}>...</ThemeProvider>;}
This will generate automatically a global stylesheet defining all CSS theme variables, allowing you to reference them in your style overrides.
:root {--mui-palette-primary-main: #1976d2;/* ...other variables */}
Benefits:
Caution: Be careful not to overdo it with customizations in the theme layer. The goal is to define global design tokens, not to override component-specific styles, which we'll cover in the next section.
What this means: When you need to customize components—whether it's modifying their APIs or altering their styles—wrap the underlaying library's component. This approach keeps your design system flexible and modular.
import cs from 'classnames';import { Button as MUIButton } from '@mui/material';// ✅ Use CSS modules to localize the styles to this componentimport styles from './Button.module.css';export default function Button(props) {// 💡 Process the props before passing them to the underlying componentreturn <MUIButton {...props} className={cs(styles.Root, props.className)} />;});
Benefits:
In summary, the best way to structure your custom design system is start with creating a centralized package/directory that exports all necessary components. Some components might simply be re-exports from an underlying UI library, while others could be a wrapper components with altered APIs or styles.
Next, define your global overrides using CSS tokens, and clearly document how to inject them.
Lastly, ensure your design system documentation includes best practices, usage guidelines, and API references to make it as user-friendly as possible.
By following these best practices, you’ll be able to create a design system that’s scalable, maintainable, and flexible enough to grow with your product’s needs.
I hope you found this guide helpful. If you have any questions or want to dive deeper into any of the topics discussed, feel free to reach out to me on social media or via email.
Happy building and Merry Xmax! 🎄