One design system.
Built to be handed over.
This is how I build a design system, step by step: the tokens, the components, the accessibility rules and the files a developer needs. Plinth is the one I built for this site, in Figma, ready for Storybook.
variables in 6 collections
component sets, 300+ variants
icons on a 24px grid
text styles, 2 fonts
A note on the images. The screenshots on this page are real grabs from the Figma file, not redrawn mockups. Some look soft at full width because of that. The Figma file is the source of truth.
Five steps.
In this order.
I used my experience working across two global design systems as a starting point for Plinth. Every value is my own: paper and ink, one red, Space Grotesk and IBM Plex Mono, square corners, hard offset shadows.
Audit what's live
Pull every colour, size and spacing value from the site. Count them. Most sites have far more values than they need.
Tokens first
Colour, type and spacing before a single component. Three tiers, so a hex code lives in exactly one place.
One button, then the rest
The button forces every decision: states, sizes, focus, icons. Once it works, other components inherit the answers.
Accessibility as a rule, not a pass
Focus, contrast, tap targets and error patterns are written into the tokens and component rules, not checked at the end.
Hand it over
Token files with names that match the Figma variables, and a props table for every component that maps to Storybook args.
Hex codes live in one place.
Nowhere else.
Three tiers. Primitives hold the raw values. Semantic tokens name the job, like text/accent. Components only ever use semantic tokens, so changing a colour or adding a theme means editing one tier.
| Tier | Holds | Modes | CSS prefix |
|---|---|---|---|
| Primitives | Raw colour scales | Value | --ri-color-red-400 |
| Semantic | 40 named jobs | Light, Dark | --ri-color-text-accent |
| Spacing, size, type | Scales and named steps | Desktop, Mobile | --ri-spacing-section |
Name the job, not the colour
text/accent tells a developer what it's for. red-400 tells them nothing. Rename the hex and nothing downstream breaks.
Same names, different pointers
Light is the original and the only mode on the live site. Dark is a proposed extension. Each semantic name points at a different primitive. Coral replaces red, and shadows and focus follow.
Two fonts. Sixteen styles.
Seven spacing names.
Type
Space Grotesk for words, IBM Plex Mono for numbers, prices, tags and specs. Display runs 120px on desktop and 44px on mobile. Body text never goes under 16px.
Spacing
A fixed scale from 2px to 96px, then seven names that do the layout. On mobile the gap drops from 24 to 16, the section from 80 to 48 and the page padding from 56 to 20. Nobody types a number.
One shadow
Flat red, no blur, always down and right. Hover lifts an element 2px up and left. There are no soft shadows anywhere.
Mobile is its own mode
Not a smaller desktop. Spacing and type switch together through variable modes below 768px, so components don't need separate mobile versions.
- Display 120px to 44px
- Page padding 56px to 20px
- Header turns into a Menu button
One button.
Sixty versions.
The button has four types, three sizes and five states. Each one is a real Figma variant, built from tokens, with a name a developer can use as a Storybook prop. Hover lifts it 2px. Focus draws a ring. Disabled is its own state, not a faded default.
37 component sets
Actions, forms, display, navigation, feedback and overlays, plus a header, footer and CTA band for sections. I left out e-commerce parts I had no use for, like swatches and star ratings.
The state you forgot to draw
Checkboxes alone have 15 states. Inputs have error and success. Toasts have four tones. A disabled control needs a reason, so the pattern says why.
37 icons, one rule
24px grid, 2px stroke, square caps. Sizes 16, 20, 24 and 32. Drawn the same way, they sit together without effort.
Accessible from
the first token.
WCAG 2.2 AA is the floor. It's decided at the token level, so every component inherits it.
You always know where you are
A 3px paper gap, then a 2.5px red ring. 5.5 to 1 against paper, 6.1 to 1 against ink. :focus-visible only, and never removed.
44px minimum
Medium controls are 44px. Large, used for primary mobile actions, is 52px. A label is part of the target, so the whole row is clickable.
Never colour alone
Errors get an icon and words. Error toasts don't auto-dismiss, because someone who needs longer to read shouldn't lose the message.
Tab twice, skip the lot
The first two tab stops on every page are skip to main content and skip to footer. Focus order follows reading order. If a ring would be clipped by its container, I draw it inside.
A real contrast catch
One Dark mode colour pair scored 4.3 to 1 and failed AA. I darkened the hex until it hit 4.8 to 1. One hex was the whole difference.
Developers get two files.
Not a screenshot.
Figma variables export as CSS custom properties with the same names. Components export as a props table that maps one to one onto Storybook args. If a developer can't tell what a token is for from its name, the name is wrong.
/* primitive */ --ri-color-red-400: #b23a2a; /* semantic, light */ --ri-color-text-accent: var(--ri-color-red-400); /* semantic, dark */ --ri-color-text-accent: var(--ri-color-red-300);
Rule for components: use semantic tokens only. Never reference a primitive from a component.
What's proposed.
What's unfinished.
- Proposed, not live: Dark mode, the blue and amber feedback scales, intermediate neutrals and the extra text styles. Everything else comes from the live site.
- Motion was added late. The live site already used 0.2s for hovers, 0.6 to 0.9s for entrances and the easing
cubic-bezier(.2,.8,.2,1), with animation off forprefers-reduced-motion. They're now variables in Figma and tokens in the CSS and JSON. - Code Connect mappings come once the Storybook components exist.