Design systems · Design engineering
Designing a design system, end to end
Token architecture, colour, typography, motion and a live component library, built on my own portfolio and shipped into production. Every button, input and toggle below is the system running. No screenshots.
Role
Design and engineering, solo
Surface
diegomunar.com
Stack
Next.js, CSS custom properties, Tailwind
Step 01
Count what is actually there
A system does not start with a palette. It starts with an inventory, because you cannot fix drift you have not measured. I wrote a shell script that greps the codebase for every colour, type size and radius, and ran it against my own site.
I had declared eight colour tokens. The script found fourteen colours in use, sixty nine distinct type sizes and nineteen radii. The tokens existed. Nothing referenced them.
Audit output, first run
A design system nobody references is a style guide. The difference is enforcement, not documentation.
Step 02
Decide the architecture before picking a single colour
The reason my first eight tokens failed is that they were named after what they looked like. --accent tells you nothing about when to use it, so people guess, and guessing produces fourteen colours. Three tiers fixes that.
Token tiers
Hover any tier to follow one value down the chain.
Primitive
accent-500 = #FE320C
The raw value. Named after what it is. Nobody builds with these directly.
Semantic
action/primary → accent-500
Named after the job. This is the only layer the interface is allowed to call.
Component
button/bg/rest → action/primary
Named after the part. Lets one component shift without touching the others.
The payoff is concrete. Changing the pressed state of every button in the product is one line in tier three. Rebranding is one ramp in tier one. Neither touches a component file.
Step 03
Colour: ramps first, decisions second
Four ramps, five stops each. Hover and pressed states are steps on the accent ramp rather than colours someone eyeballed, which is why the interface stays coherent when a new component appears.
Primitives
Hover a swatch for its contrast as text on the page background. The deep stops fail that test on purpose: they are border and fill values, and the ramp says so instead of leaving it to judgement. Click any swatch to copy its token.
Ground — every dark surface in the product
ground-900
#0c0c0b
page
ground-800
#111110
ground-700
#151514
surface
ground-600
#1b1b18
raised
ground-500
#23231f
Ink — every piece of text
ink-100
#F0EDE6
body copy
ink-200
#C9C5BC
ink-300
#9A9890
secondary
ink-400
#6F6C66
tertiary
ink-500
#4A4844
Accent — one hue, five stops, so states are derived and not invented
accent-300
#FF8A6E
accent-400
#FF5C3A
hover
accent-500
#FE320C
base
accent-600
#CC2809
pressed
accent-700
#991E07
Support — status only. Never decoration.
ok-500
#5FB08A
success
info-500
#4A8FD4
information
warn-500
#E0A03A
warning
danger-500
#E5484D
destructive
Semantic layer — the only names components may use
Hover a row to see what it resolves to. Click the token to copy it.
| Semantic token | Resolves to | Where it is allowed | |
|---|---|---|---|
| ground-900 | Page background | ||
| ground-700 | Cards, panels, wells | ||
| ground-600 | Menus, popovers, modals | ||
| ink-100 | Headings and body copy | ||
| ink-300 | Supporting copy, captions | ||
| ink-400 | Placeholders, disabled labels | ||
| ink-100 @ 8% | Dividers, resting inputs | ||
| ink-100 @ 16% | Hovered inputs, focused cards | ||
| accent-500 | Primary buttons, live links | ||
| accent-400 | Primary hover | ||
| accent-600 | Primary active | ||
| accent-500 @ 40% | Every focusable element |
Support colours carry status and nothing else. The moment green means both success and a brand accent, it stops meaning success.
Step 04
Typography: eleven steps, and a rule for each
Sixty nine sizes became eleven. I did not invent a modular scale, I derived the steps from what the site already used and collapsed neighbours closer than a pixel. A scale taken from the code describes the product that exists. An invented one describes a product you wish you had built.
Each step is fluid, so a single token covers phone through desktop and there are no breakpoint overrides for text anywhere in the codebase.
Type scale, rendered at its real size
Live at –px wide. Resize the window and every number below moves, because each step is one fluid token instead of a breakpoint stack. Click a token to copy it.
clamp(4.5rem, 13vw, 11rem)
line-height 0.92
Hero name. Once per page.
clamp(3.5rem, 7vw, 6.5rem)
line-height 0.98
Case study title
clamp(2.4rem, 5vw, 4rem)
line-height 1.06
Section opener
clamp(1.6rem, 3vw, 2.4rem)
line-height 1.15
Subsection heading
clamp(1.4rem, 2.5vw, 1.8rem)
line-height 1.25
Card title, pull quote
clamp(1rem, 1.6vw, 1.15rem)
line-height 1.65
Lead paragraph
clamp(0.95rem, 1.5vw, 1.1rem)
line-height 1.6
Body copy
clamp(0.9rem, 1.4vw, 1.05rem)
line-height 1.6
Dense body, table cells
clamp(0.85rem, 1.2vw, 0.95rem)
line-height 1.5
Captions, helper text
0.8rem
line-height 1.45
Metadata, timestamps
0.7rem
line-height 1.4
Eyebrows, tags. Uppercase only.
Step 05
Space, radius and motion are tokens too
Most systems stop at colour and type, which is why their products still feel loose. Rhythm, corner language and easing are the three things people read as quality without being able to name them.
Spacing — 4px base unit
Base unit 4px. Every gap, pad and margin in the product is one of these nine values, so vertical rhythm is a choice from a list instead of a number someone typed.
Hover a row to see it as a multiple of the base unit.
Radius
Hover to flatten a tile to a square, which is the only way to actually read a corner.
Motion — press replay
Entrances. Fast out of the gate, long settle.
Layout shifts. Slightly softer.
Anything that leaves and returns.
dur-fast
200ms
Hover, focus, colour
dur-base
300ms
Toggles, small position changes
dur-slow
600ms
Entrances, reveals
Three curves is a system. Eleven curves is eleven people each picking a favourite.
Step 06
Components: every state, drawn before it is needed
This is where systems are won or lost. A component is not the resting state, it is the full matrix. Hover, pressed, focus, disabled, loading, error. If the designer does not draw them, an engineer invents them at 6pm on a Friday.
Button — four intents by five states
| rest | hover | active | focus | disabled | |
|---|---|---|---|---|---|
| primary | Save | Save | Save | Save | Save |
| secondary | Save | Save | Save | Save | Save |
| ghost | Save | Save | Save | Save | Save |
| danger | Save | Save | Save | Save | Save |
Button — sizes, live
sm · 30px
md · 38px
lg · 46px
loading
Hover and press the row above. The transitions use dur-fast and ease-out-expo from step five.
Input — the four states that actually ship
We only use it to reply.
rest
We only use it to reply.
focus
Add a domain, like example.com.
error
Managed by your admin.
disabled
Error is a state of the field, not a red line underneath it. The border, the ring and the helper text all change together, which is why the error is legible without colour alone carrying the message.
Selection controls — live, keyboard operable
Checkbox
Radio
Toggle
Status badges
Step 07
The handoff is the prop signature
A component is only shared if its API is small enough to remember. I write the prop signature at the same time as the Figma variants, and they use the same words, so the conversation between design and engineering stops being a translation.
Anatomy — every measurement is a token
size-md
space-4 + 2
radius-base
text-sm
space-2
focus/ring
Public API
type ButtonProps = {
intent?: "primary" | "secondary" | "ghost" | "danger"
size?: "sm" | "md" | "lg"
loading?: boolean
disabled?: boolean
}Playground — change the props, watch the JSX
Variants over booleans. Four intents in one prop cannot contradict each other; four booleans can produce sixteen combinations, twelve of which are nonsense.
Step 08
Keep it alive, or it rots in a quarter
The audit script from step one is the system's immune response. It runs on every commit and prints the same numbers, so drift shows up as a diff instead of as a feeling six months later.
It also separates two surfaces, which took me two wrong runs to get right: system chrome must use tokens, while a case study recreating a client's interface must keep that client's literal colours. Counting those as debt would have pushed me to tokenise someone else's brand into mine.
What goes in the system
Does something like it already exist?
Use it. Extend it with a prop only if two more screens need the same thing.
Will three or more screens use it?
It goes in the system. Fewer than three, it stays local to its feature.
Is the change visual or behavioural?
Visual goes in a token. Behavioural goes in a prop. Never the other way around.
Can it be described without naming a screen?
If not, it is not a system component yet. It is a layout.
Step 09
What changed
| Before | After | |
|---|---|---|
| Token references in the codebase | 96 | 278 |
| Hardcoded colour values | 97 | 46 |
| Distinct colours rendered | 14 | 11 |
| Unnamed inline rgba() | 143 | 63 |
The number I care about is the first one. Adoption is the only metric that separates a design system from a folder of opinions.
Every button, input, toggle and badge on this page is the live system, running in production, not an image of one.