What is Design System and how to build it
Jean Vidal
Posted on April 4, 2023
What is Design System?
I separated two explanations to define the concept of Design System (DS), one more objective and direct, and another that brings the vision of how complete a DS can be.
Concept 1
A collection of reusable components, standardized for a given product, site or web system.
Concept 2
A complete set of design pattern (style guide) and documentation that comes with a UI tool kit, including UI patterns, UX design principles, and components.
When we look at a design system in this context, it embodies everything designers and developers need to build and scale digital products.
Some other things that you will find in a DS are:
- Brand guidelines
- Accessibility guidelines
- UI design guidelines
- Governance
- Best Practices
- Design system roadmap and releases
- Code snippets
- CSS variables and design tokens
- UI Kit (an image-based version of the design system components)
- Downloadable Resources
Design System x Pattern Library x Style Guide x Component Library
After understanding what a DS is, it is very common to confuse several concepts that are related to it, because the line of difference is very tenuous.
For that, I separated the most easily confused terms and what is their relationship with the DS.
Component Library
In short, it's a collection of UI components in a DS.
As part of the DS, it's a library of components that can reduce the risk of any variation between products or having with different components in different places.
They deal with the source code of the UI elements, being a block of reusable code that can stand alone or be part of several UI Patterns — for example, a button.
Pattern Library
In short, it's a collection of UI patterns in a design system.
As part of the DS, it's a set of design patterns for use in a company.
A group of components that designers use to solve usability issues — for example, a navigation bar with a logo, links, search form, and button.
Style Guide
It's a document that includes the design guidelines of a company, or, to a lesser extent, of a specific project, brand or product.
Provides context and instructions for the patterns and components of a DS — for example, HEX color codes, typography scales, uses, pros and cons, etc.
Problems to solve
Now that the concepts are clearer, it's very common in most projects that we have several problems that could be solved with the construction of a DS and/or with the use of some other tools that facilitate the development.
Below I will mention some of the most frequently occurring problems.
Lack of standard in implementations
When there is no standardization documentation, a DS or someone more senior to guide and validate the implementations, it's very common for there to be differences in the same implementation made by different devs.
This is understandable at some point, since each dev has their own way of programming, but when we talk about standardization, I mean extracting the maximum of the language resource to make the code more efficient, regardless of who is programming.
Let's go to the example?
Imagine that we are going to build a custom InputText
component using React with TypeScript.
Now we need to pass some properties that will be used in the input
element.
Many devs would do it like this:
interface TextInputProps {
placeholder?: string;
}
export function TextInput({ placeholder: TextInputProps}){
return (
<input placeholder={placeholder} />
)
}
This is not a recommended approach, as there are several native properties of the input
element, and thus, whenever it's necessary to use a new native property, we would have to add a new field in the properties.
But what's the best way to do this?
Using one of the principles of object orientation, inheritance!
interface TextInputProps extends InputHTMLAttributes<HTMLInputElement>{
elementoPersonalziado: string;
}
export function TextInput({ elementoPersonalziado, ...props}: TextInputProps){
return (
<input elementoPersonalizado={elementoPersonalziado} {...props} />
)
}
This way, our property will inherit — extends
— the attributes of an HTML input
element, and will even allow us to include specific properties from the custom component.
In the component's implementation, we can use the spread
operator to pass all the properties to the input
element, separating the elements that we will use for some validation or in a different way.
Having access to these standards and good practices is what will start to make a difference in the quality of our code, and for that, the dev must first be curious, constantly learn, question why things are done the way they are and challenge themselves.
In addition, it's important that there is documentation with this type of content or implementations already made with this “quality standard”, usually made by more experienced devs.
Today there are many tools that help enrich this type of content and we will see one of them later on as a solution to this problem.
CSS Conflicts
Problems with CSS are more common than we think, and they start when we lose time to think and define a name that later may conflict with another CSS in the application, and that later we will no longer remember the meaning of that nomenclature.
In addition, having to create separate files — for those who like to organize the code — starts to make our project bigger and we spend more time developing, having to access different files all the time to act on a screen with several components.
Duplicate Codes
The lack of standardization and componentization of codes brings one of the worst problems we can have, which is code duplication.
By not having accessible documentation or none at all, especially when there are many projects, it's common for devs to end up recreating a component that already exists, without even knowing that it exists.
Another very common scenario is when the dev found the component, however, it needs one more resource that the component doesn't have, and because it's not programmed correctly, it doesn't allow the dev to add one more “power” to the component, inducing the dev to create a new component just to meet your need, thus creating a new problem.
Development time
As previously mentioned, each of the identified problems increases development and maintenance time, making the team work with a smaller amount of tasks than the team is actually capable of.
Not to mention the rework and waste of time ($$).
Misalignment with Figma prototypes
In some cases, there is a UX Design team that does essential work and creates the entire prototype of the components and screens, however, for reasons of not having time, or that it will take work — usually caused by not having the componentized screens, in addition to having duplicate codes, which induces the dev to redo the entire component — , the team doesn't develop the faithful component to what was proposed.
This, in addition to wasting an excellent opportunity to have components modeled with higher quality and focused on the customer, generates a feeling of unproductivity in the team when they miss deadlines recreating codes.
FrontEnd Documentation
After the popularization of agile methodologies, some people believed that it was no longer necessary to have detailed documentation of systems and their parts.
But not quite.
In fact, we no longer have extensive and unnecessary documentation that took a long time to build and that soon fell into disuse because they were out of date.
However, agile methodologies did not inhibit or invalidate the use of documentation, but rather brought new possibilities.
Although we can consider the detailing of the tasks created in tools, such as Jira, as documentation, there is still a need to maintain a living documentation, which details the components and screens created, as well as guidelines for their use.
FrontEnd Tests
The discussion about tests is long, many think it's important, some believe it doesn't have much priority, besides there are different ones for us to create these tests.
The truth is that testing is, and always will be, important!
That said, most of the time we only think about BackEnd tests, however, FrontEnd tests should receive the same attention.
In the next topics we will see how to solve this and other problems.
How to solve
Now that we have a little understanding of the issues identified earlier, I'll address some solutions that I've found and that I understand are applicable for most scenarios.
The creation, configuration and application of these solutions isn't complex, nor is it a lot of work — just a little —, however, it requires dedication, discipline, patience and synergy from the team to organize themselves and contribute together.
Design System
The DS itself will solve many of the componentization, reuse and standardization problems.
Pros
- Allows reuse of components in different projects;
- You can configure themes for different projects without recreating components;
- Standardize codes;
- Decreases development time, as many components are already created.
Cons
- Large learning curve for those who have never created;
- While a dev can create, it still takes the entire team to get involved to scale and evolve, which takes time and effort.
Mitigation of Cons
- It can be done gradually, first creating the project and then creating the components as they are needed for use.
Tailwind CSS
What is it?
Just like Boostrap, Tailwind is a CSS framework that gives you the possibility to create layouts using a ready-made CSS framework.
In summary, the difference between the two is that as you need to customize Bootstrap more, it becomes more laborious, whereas TailwindCSS, because it has atomic classes, is like a lego game, where you can assemble each small part and create the larger parts in a much easier way.
Tailwind CSS works by checking all HTML files, JavaScript components and any other templates containing class names, and then generates the corresponding styles, then writes them to a static CSS file.
Pros
- Avoids conflicts — Mainly because there is no need to name the classes. As the classes already exist, we only need to assemble the “lego”;
- No additional file required — Avoiding creating CSS files for each component created;
- It has clear and easy-to-understand documentation on the website;
- Low learning curve;
- Atomic classes;
- Optimizes development time;
- Responsive design;
- Generates a static file with only the classes that will be used — This is one of the biggest gains, as it goes through the project and only includes the classes that will be used in the static file, also cleaning up the duplicates.
Cons
- Verbose — For those who have never used Tailwind, like me, it can be strange at first. Having many microclasses placed directly on the element, without having to define a class or generate a file, may seem strange at first, but I guarantee you will learn quickly. For me it was a matter of 1 day until I felt more comfortable, and the documentation helps a lot.
Mitigation of Cons
- Componentization — Componentization allows us to separate the needs of the system into smaller parts so that we can only apply CSS to what is needed.
- Directives — It is a powerful feature that Tailwind provides, especially for those who still prefer to create custom classes, even using Tailwind.
Example of a directive:
<style>
.btn {
@apply text-base font-medium rounded-lg p-3;
}
.btn--primary {
@apply bg-sky-500 text-white;
}
.btn--secondary {
@apply bg-slate-100 text-slate-900;
}
</style>
/*...*/
<article>
<footer class="grid grid-cols-2 gap-x-6">
<button class="btn btn--secondary">Decline</button>
<button class="btn btn--primary">Accept</button>
</footer>
</article>
Storybook
What is it?
Storybook is an opensource tool that prepares a development environment for UI components. It allows us to develop and test components in isolation from our application, as well as serving as living documentation.
Pros
- It's a living documentation of components and pages;
- Allows the creation of FrontEnd tests;
- We can use mocked data without the need for DB access;
- We can have accessibility validations;
- The main thing is to have an isolated environment (similar to Swagger).
Cons
- Learning curve — To create it is a bit laborious, and it is important to always read the documentation that helps a lot when creating.
Mitigation of Cons
- Creation can be done in phases — You can follow the construction of components and create the storybook to help with testing and tracking the creation of the component;
- After creating one, the others follow the same pattern with few variations — This makes creating storybooks much easier, because as you create one, you can reuse it in others or just follow the same model;
Practice
Now that we've seen all the theory behind the DS and the chosen tools, we understand why each one is used and what problems they solve.
Now let's get to the execution!
Setup
Structure
For the setup structure I used VITE to make it easier, however, anyone who wants to can create the structure from scratch.
Follow the steps:
1- Access the directory where the project will be created;
2- Execute the creation command;
//Creating the project with VITE
npm create vite@latest
//Steps of VITE
//1 - Set the project name => DesignSystem
//2 - Select the framework => React
//3 - Select the variant => Typescript
3- Clean up unused files and folders. You can remove the assets
folder and the index.css
and app.css
files.
Tailwind CSS
To configure Tailwind, the following steps are required:
1- Run the installation commands.
// Installing packages
npm install -D tailwindcss postcss autoprefixer
// Installing tailwindcss
// -p tor create the conf. file of postCSS
npx tailwindcss init -p
PostCSS — Bundler for CSS that automates CSS tasks.
Similar examples:
- Gulp
- Grunt
- WebPack
Autoprefixer — Library that adds some prefixes for some features that work only in specific browsers.
Examples of prefixes:
- Webkit
- Moz
2- Configure application content files.
/* ./tailwind.config.cjs */
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/**/*.tsx'
],
//...
}
3- Create the global style file, global.css
, and import the Tailwind CSS resources.
/* ./src/styles/global.css */
@tailwind base;
@tailwind utilities;
@tailwind components;
4- Import the global style in App.tsx
to be able to use Tailwind CSS in pages.
/* ./src/App.tsx */
import './styles/global.css';
export function App() {
return (
<h1 className='font-bold text-5xl text-violet-500'>Hello World</h1>
)
}
Font
Below are the steps to configure the fonts:
1- Fetch the DS font definitions in the project's Figma.
2- Get matching links and import in index.html, or import locally.
<!-- ./index.html -->
<!-- Roboto 400 -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.cdnfonts.com/css/mark-pro?styles=78509,78516" rel="stylesheet" />
<!-- Mark Pro 400 e 500 -->
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet" />
3- Set default font in Tailwind configuration file. We can extend — extend
—, keeping Tailwind's default settings and adding our own, or just using ours.
/* ./tailwind.config.cjs */
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/**/*.tsx'
],
theme: {
extend: {
fontFamily: {
roboto: 'Roboto, sans-serif',
markpro: 'Mark Pro, sans-serif',
}
},
},
plugins: [],
}
Storybook
Now the steps to set up the storybook:
1- Run the install command.
//Starting the Storybook
npx sb init --builder @storybook/builder-vite --use-npm
//or
npx storybook init --builder @storybook/builder-vite --use-npm
2- Clean folder and files that won't be used. We can remove the src/stories
folder and its files.
3- The .storybook/main.cjs
file indicates the directory and files that the storybook will look for to put as documentation.
/* .storybook/main.cjs */
module.exports = {
"stories": [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
//...
}
4- Import global css into Storybook.
/* .storybook/preview.cjs */
import '../src/styles/global.css';
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
}
Tokens
Finally, we'll configure our DS tokens, which is basically passing Figma's token definitions to code.
1- Take the Design System's definitions of colors, sizes and spacing in Figma.
2- Import the tokens in the configuration file.
Note: The Tailwind CSS already has default tokens that can be used, extended or replaced.
Color Tokens:
/* ./tailwind.config.cjs */
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/**/*.tsx'
],
theme: {
colors: {
brand: {
primary: {
darkest: '#0068AB',
dark: '#0077B3',
light: '#EDF8FD',
},
secondary: {
darkest: '#002C50',
dark: '#0C395D',
light: '#C7E1EE',
},
background: {
DEFAULT: '#F8F8F8',
}
},
//...
},
extend: {
fontFamily: {
roboto: 'Roboto, sans-serif',
markpro: 'Mark Pro, sans-serif'
}
},
},
plugins: [],
}
Size Tokens:
/* ./tailwind.config.cjs */
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/**/*.tsx'
],
theme: {
fontSize: {
xs: ['12px', {
fontWeight: '400',
lineHeight: '18.75px',
}],
sm: ['14px', {
fontWeight: '400',
lineHeight: '22px',
}],
base: ['14px', {
fontWeight: '400',
lineHeight: '16px',
}],
lg: ['16px', {
fontWeight: '500',
lineHeight: '28px',
}],
xl: ['24px', {
fontWeight: '500',
lineHeight: '22px',
}],
},
colors: {
//...
},
extend: {
//...
},
},
/...
}
Building a component
With all preparation finished, all that remains is to assemble the parts.
Figma + React + Tailwind CSS
The process is basically looking at the component details in Figma — let's face it, Figma helps a lot with this — and transferring it to the code.
In this example below, we extracted the color information from the background of the component and from the text part, the sizes, font data (family, weight, size), etc.
But how is it in the code?
/* ./src/components/DatePicker/DatePicker.jsx */
<div
className={`
bg-neutral-light-base
w-[109px]
h-[35px]
border-solid
border-[1px]
rounded-[20px]
border-brand-primary-dark
cursor-pointer
select-none
flex
items-center
${isMounted && 'bg-brand-primary-dark bg-opacity-[0.15]'}
`}>
<div
className="
w-full
font-roboto
text-sm
text-brand-primary-dark
text-center
py-2
"
onClick={(e) => { ref?.current?.toggle(e) }}
>
{ label }
</div>
{/*...*/}
</div>
Storybook
For Storybook, imagination is the limit!
We can display the different states of our component, detailing the parameters and in some cases allowing the user to change the data and test the different behaviors.
All this while maintaining a living and functional documentation, with the isolated component, even before being used on any screen.
In this example, I used two forms:
- With template, to reuse the same code in the variants and change only some data;
- Without the template, to set up a different scenario with the 3 variants at the same time.
It is worth remembering that the way to implement this example, which is implemented without the template, was my choice, and the storybook itself helps with similar examples using the decorator
or the canvas
and reusing the stories
.
Then evaluate each scenario and choose the one that is relevant to your needs.
/* ... */
export default {
title: 'Design System/Componentes/DatePicker',
component: DatePicker,
args: {
label: 'Lorem Ipsum',
dados: mock
},
argTypes: {
dados: { control: 'object' }
}
} as ComponentMeta<typeof DatePicker>;
const DatePickerTemplate: ComponentStory<typeof DatePicker> = (args) => {
const { label, variant } = args;
const [ dadosRetorno, setDadosRetorno ] = useState<DatePickerSelected | null>(null);
const hasSelected = (variant: number) => {
return ((dadosRetorno?.periodoDatas.length > 0) && dadosRetorno?.variant === variant);
}
return (
<DatePicker
label={label}
hasSelected={hasSelected(variant)}
variant={variant}
dados={mock}
setDadosRetorno={setDadosRetorno}
/>
)
};
/*
Reusing the template for variants
and changing only the necessary data
*/
export const Data = DatePickerTemplate.bind({});
Data.args = {
label: 'Data',
variant:1
};
Data.argTypes = {
variant: {
table: {
disable: true
},
},
hasSelected: {
table: {
disable: true
},
},
setDadosRetorno: {
table: {
disable: true
},
}
};
Data.storyName = 'Data';
export const DiaUtil = DatePickerTemplate.bind({});
DiaUtil.args = {
label: 'Prazo (DU)',
variant:2
};
DiaUtil.argTypes = {...Data.argTypes};
DiaUtil.storyName = 'Dias Úteis';
export const DiaCorrido = DatePickerTemplate.bind({});
DiaCorrido.args = {
label: 'Prazo (DC)',
variant:3
};
DiaCorrido.argTypes = {...Data.argTypes};
DiaCorrido.storyName = 'Dias Corridos';
/*
Creating a new way without the template.
*/
export const DatasSimultaneas = () => {
const [ dadosRetorno, setDadosRetorno ] = useState<DatePickerSelected | null>(null);
const hasSelected = (variant: number) => {
return ((dadosRetorno?.periodoDatas.length > 0) && dadosRetorno?.variant === variant);
}
return (
<div className='flex flex-col gap-1'>
<div className='flex flex-row gap-1'>
<DatePicker
label='Datas'
hasSelected={hasSelected(1)}
variant={1}
dados={mock}
setDadosRetorno={setDadosRetorno}
/>
<DatePicker
label='Prazo (DU)'
hasSelected={hasSelected(2)}
variant={2}
dados={mock}
setDadosRetorno={setDadosRetorno}
/>
<DatePicker
label='Prazo (DC)'
hasSelected={hasSelected(3)}
variant={3}
dados={mock}
setDadosRetorno={setDadosRetorno}
/>
</div>
</div>
);
};
DatasSimultaneas.argTypes = {
label: {
table: {
disable: true
},
},
...Data.argTypes
};
DatasSimultaneas.parameters = {
controls: { hideNoControlsWarning: true },
};
And this is the result:
We're done here!
I hope you enjoyed all the content, although a little extensive it's essential that we understand.
I recommend that you do yours, test and create different scenarios, because this experience goes beyond the needs of the company you are working for. It will give you a lot of baggage and knowledge about the systems we build, about how we see these systems and especially a horizon on scalability.
See you later!
References
Design System
Tailwind CSS
Storybook
Posted on April 4, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.