@MaximeHeckel

Building a Design System from scratch

May 10, 2022 / 24 min read

Last Updated: November 11, 2023

As someone who's always been striving for consistency, building delightful and accessible experiences, and trying to do all that faster than ever, the concept of ✨design systems✨ has always interested me. I believe in setting up clear standards for colors and fonts and establishing patterns to build reusable components as the key to building sustainable UIs that can withstand the test of time.

For the past few years, I've been working a lot on this blog, the interactive experiences showcased in my blog posts, and several other tiny projects that needed consistency in branding and components. The more I worked on them, the more I felt the need to stop copy-pasting code and colors between projects and needed my own set of UI pieces: my personal design system.

After pouring countless hours into this project and sharing my progress over the past several months (almost a year now actually!), I felt it was time to write a little return on experience to focus on all the things I've learned while building a design system on my own 😊. So in this blog post, I'll go through the component patterns I came up with, explain how I picked up my tokens and overall the design system thinking mentality I adopted to make this project (somewhat) successful.

Context: Why would I even choose to build my own design system?

Before jumping on the actual building part of this blog post, I first want to give a bit more context on why I chose to dedicate time to this project. Among the many reasons why this project came to life, you will mainly find:

  • ArrowAn icon representing an arrow
    Branding: I'm trying very hard to be unique in an endless sea of developers' blogs/websites looking more or less the same. I want people to recognize my work from afar through my choice of colors, logo, components design, playfulness, and attention to detail.
  • ArrowAn icon representing an arrow
    Consistency: Each piece composing this system should have a purpose. All components follow the same guidelines and are composed of more primitive elements/tokens.
  • ArrowAn icon representing an arrow
    Fun and Learning: I learned a lot about component building, design system thinking, and myself while building this tiny library. It helped me develop some empathy and step back and think twice about the interface of a component, composability, abstraction, and scalability. Focusing on one piece of the system at a time and making that one component mine was tons of fun and very satisfying.

This project was not a necessity per se, but the more my blog/portfolio and brand were evolving, the more I was striving for these things, and the more not having a design system was slowing me down. I needed my own set of "Lego pieces" that I could rearrange/combine infinitely. Thus the idea of building a personal design system came to my mind:

A small scoped design system mainly composed of primitive components focused solely on personal branding and personal use.

Even though the scope of this design system feels small compared to the bigger ones we can get to work on in a work context, it was not necessarily less complex to build. In the following parts, I'll go through the challenges and decisions I've made along the way when working on this project.

Tokens

Tokens are the discrete elements of styles like color palette, spacing units, shadows, or typography that form the foundation of a design system. Breaking down my different projects into these most fundamental pieces was essential when I started working on my design system.

Color system

First, I wanted to define an efficient solid color system. I ended up opting for what I dubbed a "two-tier color variable system":

  1. ArrowAn icon representing an arrow
    The first layer is a series of variables representing the HSL (Hue, Saturation, Lightness) values of the different colors within the palettes like --blue-10: '222, 89%, 90%' or --red-60: 0, 95%, 40%.
  2. ArrowAn icon representing an arrow
    The second layer is more of a generic alias to the colors that will end up referenced by the components of the design system: --brand: hsl(var(--blue-50)) or --foreground: hsla(var(--gray-05), 60%). In this layer, we use the colors defined in the first one and compose them or expand them.
Diagram illustrating the two-tier variable system: the --brand color is used as background color in the button while referencing the color itself.

This system worked for me for the following reasons:

  • ArrowAn icon representing an arrow
    Components never end up referencing actual "colors" per se: the background color of the Button component is not --blue-10 but --brand and the value of that variable may evolve through time from blue to purple or anything else. Thanks to this system, components are more resilient to change: want to change the brand color? All you need to do is update the value of the --brand variable, and all the components referencing it will update accordingly.
  • ArrowAn icon representing an arrow
    It lets me compose my color tokens, like adding some opacity. I talked about all this in a dedicated blog post: The Power of Composition with CSS variables where I showcase a few of my color composition patterns.
  • ArrowAn icon representing an arrow
    Building themes like light and dark mode easily: in light mode --brand might reference --blue-60, in dark mode it will be --blue-20.

To illustrate the steps I took to pick up colors, create a palette, and come up with tokens, I built the little animated slideshow ✨ below:

Step 1: Pick base colors

Other tokens

Colors variables were my main focus to get started. They are perhaps the most crucial set of tokens to start building a compelling visual language. Then came the necessity to define consistent spacing units:

Spacing tokens

1
--space-0: 0px;
2
--space-1: 4px;
3
--space-2: 8px;
4
--space-3: 12px;
5
--space-4: 16px;
6
--space-5: 24px;
7
--space-6: 32px;
8
--space-7: 40px;
9
--space-8: 48px;
10
--space-9: 56px;
11
--space-10: 64px;
12
--space-11: 80px;
13
--space-12: 96px;

and font related tokens:

Typography tokens

1
--font-size-1: 0.75rem;
2
--font-size-2: 0.875rem;
3
--font-size-3: 1rem;
4
--font-size-4: 1.125rem;
5
--font-size-5: 1.25rem;
6
--font-size-6: 1.5rem;
7
--font-size-7: 2rem;

and little things like border radii:

Radii tokens

1
--border-radius-0: 4px;
2
--border-radius-1: 8px;
3
--border-radius-2: 16px;

Components reference these tokens directly as they are less likely to be changing significantly over time.

Lessons learned

As I iterated on the components and developed common patterns, I often had to go back to the drawing board and define new tokens, redefine/refine some other ones, or combine and delete some. This process was particularly tedious for me as:

  • ArrowAn icon representing an arrow
    Unlike my experience working on a design system in a professional context, I do not have a designer working on this one. I could only rely on gut feeling or trial and error until it felt like I nailed it or defined something that looked great.
  • ArrowAn icon representing an arrow
    I imposed a rule on myself: containing the number of tokens as much as possible. That was, at times, really hard as I needed to preserve a balance between the "complexity of my design system" and the level of consistency.

The tokens I've defined so far will most likely evolve in the future as I'm expanding the number of components or experimenting with new colors or new ways to define variables. I learned through this project to see them more as a malleable layer of a design system instead of a solid bedrock where everything sits on top.

Component patterns

As of today, my design system contains only simple components or primitives. All I need is a set of simple pieces that lets me build things faster, with consistency, while still allowing some wiggle room for creativity: like a Lego kit. Thus, I optimized this project to preserve a balance of:

  • ArrowAn icon representing an arrow
    Good developer experience (DX). I want my components to be useful and help me work, experiment, and iterate faster.
  • ArrowAn icon representing an arrow
    Beautiful and cohesive design/design language. Thus allowing components to be composed not just on the code side of things but also visually.

I'm dedicating this part to showcasing some patterns and tricks I've come up with to achieve these goals while also making the components of my design system easier to use and maintain. If you're into component DX and composition patterns, this section should scratch an itch ✨.

Variant driven components

I've always been a big fan of styled components and wanted them to be at the core of this design system. This time, however, I opted for something a bit more opinionated: @stitches/react.

Among the many reasons why I picked this one rather than a more widely adopted library are:

  • ArrowAn icon representing an arrow
    The variant-driven approach. Stitches emphasize the use of variants. The set of variants a given component supports must be predefined, which means no dynamic props are allowed for styling. I'm a big believer in this pattern when working on a design system. It makes you really think about developer experience and the interface of your components. I did my best to keep the number of variants down and privilege composition and compound components which I will detail later in this article.
  • ArrowAn icon representing an arrow
    The support for polymorphism. Stitches lets you override the tag of a component via a polymorphic as prop. I'll showcase some examples of that pattern below.
  • ArrowAn icon representing an arrow
    The advanced Typescript support. Styled components' variants come with types automatically. There's no extra work needed.

Sample component showcasing the main features of Stitches

1
import { styled } from '@stitches/react';
2
3
const Block = styled('div', {
4
borderRadius: 8px;
5
height: '50px';
6
width: '100%';
7
display: 'flex';
8
justifyContent: 'center;
9
alignItems: 'center';
10
11
variants: {
12
/* the appearance prop will be automatically typed as 'primary' | 'secondary' */
13
appearance: {
14
'primary': {
15
background: 'blue';
16
color: 'white';
17
},
18
'secondary': {
19
background: 'hotpink';
20
color: 'white';
21
}
22
}
23
}
24
25
/* specifying a default variant will make the appearance prop optional */
26
defaultVariant: {
27
appearance: 'primary';
28
}
29
});
30
31
32
const App = () => {
33
return (
34
<Block as="section" appearance="secondary">
35
Styled-components
36
</Block>
37
)
38
}

When it comes to writing actual styles, I wrote my fair share of spaghetti CSS throughout my career, and I did not want this project to end up the same way. Luckily, Stitches keeps my styled-components code in check whether it's pattern-wise (no dynamic props, only variants) or type-wise, and makes me avoid many of the pitfalls I fell into with other styled-components libraries. On top of that, I came up with some custom patterns/rules to further improve the readability and maintainability of my code.

One pattern that I kept getting back to while building my components was relying on local CSS variables to handle transitions, and hover/focus/active states.

Button component using local CSS variables

1
import { styled } from '@stitches/react';
2
3
const StyledButton = styled('button', {
4
/* Initializing local variables first and assigning them default values */
5
background: 'var(--background, white)',
6
color: 'var(--color, black)',
7
boxShadow: 'var(--shadow, none)',
8
opacity: 'var(--opacity, 1)',
9
transform: 'scale(var(--button-scale, 1)) translateZ(0)',
10
11
/* Main styles of the component */
12
padding: 'var(--space-3) var(--space-4)',
13
fontSize: 'var(--font-size-2)',
14
fontWeight: 'var(--font-weight-500)',
15
height: '44px',
16
width: 'max-content',
17
transition: 'background 0.2s, transform 0.2s, color 0.2s, box-shadow 0.3s',
18
borderRadius: 'var(--border-radius-1)',
19
20
/* Update local variables based on state/variant */
21
'&:active': {
22
'--button-scale': 0.95,
23
},
24
25
'&:disabled': {
26
'--background': 'var(--form-input-disabled)',
27
'--color': 'var(--typeface-tertiary)',
28
},
29
30
'&:hover': {
31
'&:not(:disabled)': {
32
'--shadow': 'var(--shadow-hover)',
33
},
34
},
35
'&:focus-visible': {
36
'--shadow': 'var(--shadow-hover)',
37
},
38
39
variants: {
40
variant: {
41
primary: {
42
'--background': 'var(--brand)',
43
'--color': 'var(--typeface-primary)',
44
},
45
secondary: {
46
'--background': 'var(--brand-transparent)',
47
'--color': 'var(--brand)',
48
},
49
},
50
},
51
});

You can see in the snippet above that:

  • ArrowAn icon representing an arrow
    The local variables used in this component sit at the top. This is where I initialize them with default values.
  • ArrowAn icon representing an arrow
    Then, I follow up with the main body of the CSS which contains all the main CSS properties.
  • ArrowAn icon representing an arrow
    Then, any nested code, variants, selectors, ::before, or ::after statements only reassign those CSS variables.

The resulting code is much easier to read and I am less afraid to experiment with more complex CSS code without feeling like I am giving up on maintainability.

Utility components

Since the objective of this design system was to enable faster work/experimentations, I came up with a set of utility components. These components range from:

  • ArrowAn icon representing an arrow
    Box. The primordial component of the design system. It's mainly an empty shell that I use as an enhanced div that supports the Stitches css prop. It's useful for quickly prototyping without having to edit multiple files.

Box component

1
import { styled } from '@stitches/react';
2
3
const Box = styled('div', {});
4
5
/* Usage with `css` prop on the fly */
6
7
const App = () => {
8
return (
9
<Box
10
css={{
11
background: 'var(--brand-transparent)';
12
color: 'var(--typeface-primary)';
13
borderRadius: 'var(--border-radius-1)';
14
width: 100,
15
height: 100,
16
}}
17
/>
18
)
19
}
  • ArrowAn icon representing an arrow
    Flex and Grid. These are my layout utility components. They aim to quickly create flex and grid CSS layouts. They come with predefined variants/props to help set some of their unique properties like alignItems, justifyContent, gap, or columns. These slowly became life savers in the codebases that use my design system. They allow me to build prototypes with complex layouts in no time.
1
const App = () => {
2
return (
3
<>
4
<Flex
5
alignItems="center"
6
direction="column"
7
justifyContent="center"
8
gap="2"
9
>
10
<Box css={...} />
11
<Box css={...} />
12
</Flex>
13
<Grid gap="4" templateColumns="repeat(2, 1fr)">
14
<Box css={...} />
15
<Box css={...} />
16
<Box css={...} />
17
<Box css={...} />
18
</Grid>
19
</>
20
);
21
};
  • ArrowAn icon representing an arrow
    Text. Keeping anything typography-related throughout any project I have undertaken has always been a challenge. Thus, to solve this issue, I created this utility component. It has dedicated variants for sizes, colors, weights, and neat little utility props like truncate or ✨gradient✨ which have been lifesavers many times. I appreciate using this component daily and ended up composing many more specific typography components on top of it.
Almost before we knew it, we had left the ground.
Almost before we knew it, we had left the ground.
Almost before we knew it, we had left the ground.
1
const App = () => {
2
return (
3
<>
4
<Text outline size="6">
5
Almost before we knew it,
6
we had left the ground.
7
</Text>
8
<Text truncate>
9
Almost before we knew it,
10
we had left the ground.
11
</Text>
12
<Text
13
gradient
14
css={{
15
backgroundImage:
16
'linear-gradient(...)',
17
}}
18
size="6"
19
weight="4"
20
>
21
Almost before we knew it,
22
we had left the ground.
23
</Text>
24
</>
25
);
26
};
  • ArrowAn icon representing an arrow
    VisuallyHidden. The CSS to visually hide an element is very hard to remember. So I created a component to not have to Google it every so often 😄. It helps me add additional text for assistive technologies to elements can have more context when needed.

Compound components

I love compound components. I even dedicated three different articles about them 😄 (that are a bit dated now). I believe coming up with a nice set of compound components can significantly improve the DX of a given component.

There were two use cases where I ended up opting for compound components:

  1. ArrowAn icon representing an arrow
    When, if not split into smaller related components, the prop interface would be overloaded.
  2. ArrowAn icon representing an arrow
    When the component could potentially be composed in many ways.

Among some of the components that ended up leveraging a compound components pattern are:

  • ArrowAn icon representing an arrow
    Radio
1
<Radio.Group name="options" direction="vertical" onChange={...}>
2
<Radio.Item
3
id="option-1"
4
value="option1"
5
aria-label="Option 1"
6
label="Option 1"
7
/>
8
<Radio.Item
9
id="option-2"
10
value="option2"
11
aria-label="Option 2"
12
label="Option 2"
13
checked
14
/>
15
</Radio.Group>
  • ArrowAn icon representing an arrow
    Card
1
<Card>
2
<Card.Header>Title of the card</Card.Header>
3
<Card.Body>Content of the card</Card.Body>
4
</Card>

Some of my compound components are more restrictive than others when it comes to the types of components that can be rendered within them as children. In the case of Card, I chose flexibility since I didn't want to "gate" its use. For Radio, however, I felt the need to prescribe how to use it, and for that, I built the following little utility:

isElementOfType utility function

1
export function isElementOfType(element, ComponentType): element {
2
return element?.type?.displayName === ComponentType.displayName;
3
}

This function lets me filter the components rendered under Radio based on the displayName of the child:

Using isElementOfType to filter out invalid children

1
import RadioItem from './RadioItem';
2
3
const RadioGroup = (props) => {
4
const { children, ... } = props;
5
6
const filteredChildren = React.Children.toArray(children).filter((child) =>
7
isElementOfType(child, RadioItem);
8
);
9
10
return (
11
<Flex gap={2} role="radiogroup">
12
{filteredChildren}
13
</Flex>
14
)
15
}

Polymorphism and composition

Using composition results in more abstract components that require fewer props than their primitive counterpart and have a more narrow use case. When done well, they can increase developer velocity and make a design system even easier to use. Given the wide range of applications this design system could have, and how primitive its pieces are, I wanted to optimize for composition and extensibility from the start. Luckily for me, picking the @stiches/react library proved to be a great choice due to its support for polymorphism through the as prop.

The as prop allows picking which tag a component renders. I expose it in many of my utility components, like Text for example:

1
// Renders a p tag
2
<Text as="p">Hello</Text>
3
4
// Renders an h1 tag
5
<Text as="h1">Hello</Text>

Not only these components can take any HTML tag in their as prop, but I found many use cases where more specifying other components makes perfect sense:

1
<Card>
2
{/* Card.Body inherits the style, the props and the type of Flex! */}
3
<Card.Body as={Flex} direction="column" gap="2">
4
...
5
</Card.Body>
6
</Card>

The code snippet above showcases theCard.Body compound component rendered as a Flex component. In this case, not only does Card.Body inherits the styles, but it also inherits the props and the types! 🤯

It does not stop there! On top of allowing for polymorphism, my styled-components are also built to be composed:

Composed components originating from Text

1
const DEFAULT_TAG = 'h1';
2
3
const Heading = () => {
4
// Remapping the size prop from Text to a new scale for Heading
5
const headingSize = {
6
1: { '@initial': '4' },
7
2: { '@initial': '5' },
8
3: { '@initial': '6' },
9
4: { '@initial': '7' },
10
};
11
12
// Overriding some styles of Text based on the new size prop of Heading
13
const headingCSS = {
14
1: {
15
fontWeight: 'var(--font-weight-600)',
16
lineHeight: '1.6818',
17
letterSpacing: '0px',
18
marginBottom: '1.45rem',
19
},
20
2: {
21
fontWeight: 'var(--font-weight-600)',
22
lineHeight: '1.6818',
23
letterSpacing: '0px',
24
marginBottom: '1.45rem',
25
},
26
3: {
27
fontWeight: 'var(--font-weight-600)',
28
lineHeight: '1.6818',
29
letterSpacing: '0px',
30
marginBottom: '1.45rem',
31
},
32
4: {
33
fontWeight: 'var(--font-weight-600)',
34
lineHeight: '1.6818',
35
letterSpacing: '0px',
36
marginBottom: '1.45rem',
37
},
38
};
39
40
return (
41
<Text
42
as={DEFAULT_TAG}
43
{...rest}
44
ref={ref}
45
size={headingSize[size]}
46
css={{
47
...merge(headingCSS[size], props.css),
48
}}
49
/>
50
);
51
};
52
53
// Creating a more abstracted version of Heading
54
const H1 = (props) => <Heading {...props} as="h1" size="4" />;
55
const H2 = (props) => <Heading {...props} as="h2" size="3" />;
56
const H3 = (props) => <Heading {...props} as="h3" size="2" />;
57
const H4 = (props) => <Heading {...props} as="h4" size="1" />;

This allows me to create more abstracted and narrow focused components out of the primitives of the design system.

Make it shine!

The final look and feel of the whole system is, to my eyes, as essential as the DX. I built these pieces not only to build faster but also to build prettier. On top of the colors and the little details such as:

I sprinkled some subtle, yet delightful, micro-interactions inspired by some of the work of @aaroniker_me in my components:

Type a fake email like "hello@test.co".
Click on the "Reveal Password" button.
Hover, press and hold!

Adding those little details made this project fun and kept me going. Using them on other projects and this blog brings me joy ✨.

Packaging and shipping

In this last part, I want to focus on the shipping aspect of a design system such as:

  • ArrowAn icon representing an arrow
    Packaging patterns, and which one I ended up picking.
  • ArrowAn icon representing an arrow
    File structure.
  • ArrowAn icon representing an arrow
    Bundling and releasing.

Versioning

Should you build an individual library? Or have one package per component? These are valid questions when thinking about how your projects will consume your design system.

Since I optimized for simplicity throughout this project, I chose to have one package for my entire design system: @maximeheckel/design-system. Thus, I'd only have to ever worry about versioning this one library. However, this came with one major pitfall: I now had to make my package tree shakable so importing one component of my design system would not result in a big increase in bundle size on my projects.

If you're curious about other versioning/packaging patterns along with their respective advantages and drawbacks I'd recommend checking out Design System versioning: single library or individual components? from @brad_frost. It's an excellent read, and it helped me through my decision process for the versioning of this project.

File structure

When it comes to file structures, I found a lot of inspiration in @JoshWComeau's proposal in one of his latest blog posts titled Delightful React File/Directory Structure. Some of his decisions made sense to me and I highly encourage reading it!

Bundling

For bundling, I picked up esbuild. I got to play with my fair share of bundlers throughout my career, but nothing comes close to the speed of esbuild. I can bundle my entire design system (excluding Typescript type generation) in barely a second. Without having much prior experience with esbuilt itself, I still managed to come up with a working configuration relatively fast:

My current esbuild config

1
const esbuild = require('esbuild');
2
const packagejson = require('./package.json');
3
const { globPlugin } = require('esbuild-plugin-glob');
4
5
const sharedConfig = {
6
loader: {
7
'.tsx': 'tsx',
8
'.ts': 'tsx',
9
},
10
outbase: './src',
11
bundle: true,
12
minify: true,
13
jsxFactory: 'createElement',
14
jsxFragment: 'Fragment',
15
target: ['esnext'],
16
logLevel: 'debug',
17
external: [...Object.keys(packagejson.peerDependencies || {})],
18
};
19
20
esbuild
21
.build({
22
...sharedConfig,
23
entryPoints: ['src/index.ts'],
24
outdir: 'dist/cjs',
25
format: 'cjs',
26
banner: {
27
js: "const { createElement, Fragment } = require('react');\n",
28
},
29
})
30
.catch(() => process.exit(1));
31
32
esbuild
33
.build({
34
...sharedConfig,
35
entryPoints: [
36
'src/index.ts',
37
'src/components/**/index.tsx',
38
'src/lib/stitches.config.ts',
39
'src/lib/globalStyles.ts',
40
],
41
outdir: 'dist/esm',
42
splitting: true,
43
format: 'esm',
44
banner: {
45
js: "import { createElement, Fragment } from 'react';\n",
46
},
47
plugins: [globPlugin()],
48
})
49
.catch(() => process.exit(1));

Here are some of the main takeaways from this config:

  • ArrowAn icon representing an arrow
    esbuild does not provide any JSX transform feature or plugin like Babel does. I had to define a jsxFactory (L13-14) and jsxFragment option as a workaround.
  • ArrowAn icon representing an arrow
    On the same note, I also had to add the react import/require statements through the banner option. It is not the most elegant thing, but it's the only way I could make this package work.
  • ArrowAn icon representing an arrow
    I bundled this package in both ESM and CJS format.
  • ArrowAn icon representing an arrow
    ESM supports tree-shaking, hence, why you'll see multiple entryPoints (L35-40) provided in this section of the config.

Thanks to this configuration, I had a way to generate a tree-shakable package for my design system in seconds. This allowed me to fix to biggest drawback of using a single package: no matter what you'll import from the design system, only what's imported will end up bundled in the consumer project.

1
// This will make the project's bundle *slightly* heavier
2
import { Button } from '@maximeheckel/design-system';
3
4
// This will make the project's bundle *much* heavier
5
import { Button, Flex, Grid, Icon, Text } from '@maximeheckel/design-system';

Releasing

For the release process of this project, I opted for a semi-manual approach for now:

  • ArrowAn icon representing an arrow
    Releases are triggered manually on Github via a repository dispatch event.
  • ArrowAn icon representing an arrow
    I select the branch and the release type (major/minor/patch) based on the versioning rules I established earlier.
  • ArrowAn icon representing an arrow
    A Github workflow then starts and will bump the version based on the selected release type and publish the package on NPM.

I will most certainly iterate on this whole process very soon:

  • ArrowAn icon representing an arrow
    I still do not have a proper CI process for this project.
  • ArrowAn icon representing an arrow
    I don't even have a Storybook where I can publish and compare different versions of my design system components. This is still on my TODO list.
  • ArrowAn icon representing an arrow
    I would love to automate the release process even further using libraries like Semantic Release.

This will most likely deserve a standalone blog post 👀 as there's a lot to talk about on this subject alone. In the meantime, you can head out to the repository of this project to check out the current release workflow.

Conclusion

As of writing these words, this project is still a work in progress. The resulting package is already actively being used on this blog and my upcoming portfolio (which is yet another massive project I have in progress). There's, however, still a lot left to do before I could publish what I could consider a good v1.0! Among the things left are:

  • ArrowAn icon representing an arrow
    Migrating the rest of the components to @maximeheckel/design-system.
  • ArrowAn icon representing an arrow
    Providing more primitive components such as Modal or Tabs.
  • ArrowAn icon representing an arrow
    Including a couple of utility React hooks that I use in all my projects like useDebounce or useKeyboardShortcut.
  • ArrowAn icon representing an arrow
    More experimentations with little micro-interactions to provide the best experience to the people visiting my sites. (and that includes you 😄!)
  • ArrowAn icon representing an arrow
    Coming up with a great CI process, to visually test my components and avoid regressions: stay tuned for a potential dedicated blog post for this one 👀.
  • ArrowAn icon representing an arrow
    Build a dedicated project page for the design system on my portfolio.

Right now, the set of primitive and utility components I have available through my design system is already helping me work faster and build consistent experiences. For more complex components, I'd lean towards using Radix UI as a solid base rather than building them from scratch. Time will tell what UI pieces I will eventually need.

It would be an understatement to qualify this design system as a daunting task. I spent on/off a couple of months on it, and it was sometimes frustrating, especially when coming up with the right tokens, but I still had a lot of fun working on this project and the result is worth it! I now have a working personal design system that gives me all the tools and components to build consistent experiences, and I can't wait to see how it will evolve.

Liked this article? Share it with a friend on Bluesky or Twitter or support me to take on more ambitious projects to write about. Have a question, feedback or simply wish to contact me privately? Shoot me a DM and I'll do my best to get back to you.

Have a wonderful day.

– Maxime

A deep dive into my experience building my own design system that documents my process of defining tokens, creating efficient components, and shipping them as a package.