Case study · Plinth

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.

164

variables in 6 collections

37

component sets, 300+ variants

37

icons on a 24px grid

16

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.

Approach

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.

  1. 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.

  2. Tokens first

    Colour, type and spacing before a single component. Three tiers, so a hex code lives in exactly one place.

  3. One button, then the rest

    The button forces every decision: states, sizes, focus, icons. Once it works, other components inherit the answers.

  4. 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.

  5. Hand it over

    Token files with names that match the Figma variables, and a props table for every component that maps to Storybook args.

Tokens

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.

TierHoldsModesCSS prefix
PrimitivesRaw colour scalesValue--ri-color-red-400
Semantic40 named jobsLight, Dark--ri-color-text-accent
Spacing, size, typeScales and named stepsDesktop, Mobile--ri-spacing-section
figma.com/design · Primitives
Primitive colour scales in Figma
Primitives: neutral, red, green, plus proposed blue and amber for feedback states.
Naming

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.

Dark mode Proposed

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.

figma.com/design · Semantic colour, Light
Semantic colour tokens in Light mode
Semantic tokens, Light mode. Each row maps a job to a CSS variable.
figma.com/design · Semantic colour, Dark (proposed)
Semantic colour tokens in Dark mode
The same names in Dark mode. A proposal, not part of the live site.
Type and spacing

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.

figma.com/design · Type
Type styles with desktop and mobile sizes
Display, heading, title and the rest. Desktop and Mobile sizes sit in one mode-switched variable set.

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.

figma.com/design · Spacing
Named spacing steps, desktop versus mobile
Named steps, Desktop versus Mobile. Mode switches below 768px.

One shadow

Flat red, no blur, always down and right. Hover lifts an element 2px up and left. There are no soft shadows anywhere.

figma.com/design · Elevation
Four hard offset shadow sizes
Four offset sizes, one colour.

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
Components

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.

figma.com/design · Button
Button variants across types and states
Types down, states across. Primary, secondary, inverse and link.
Coverage

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.

States

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.

Icons

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.

figma.com/design · Icons
The 37 icons on a 24px grid
All 37 icons on the shared grid.
figma.com/design · Checkboxes
Checkbox states and rules
Checked, unchecked, indeterminate, across hover, focus, error and disabled. Each page ends with its own rules.
Accessibility

Accessible from
the first token.

WCAG 2.2 AA is the floor. It's decided at the token level, so every component inherits it.

Focus

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.

Targets

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.

Signals

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.

figma.com/design · Focus
Focus ring with rules
The focus ring, with the rules written next to it.

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.

contrast check
Contrast check: 4.3 to 1 fails AA, 4.8 to 1 passes
Handoff

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);
Figma property to Storybook arg
Table mapping Figma properties to Storybook args
Every component set carries its Storybook name and args in its description, and each page has a props table.

Rule for components: use semantic tokens only. Never reference a primitive from a component.

Honest notes

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 for prefers-reduced-motion. They're now variables in Figma and tokens in the CSS and JSON.
  • Code Connect mappings come once the Storybook components exist.
Short video breakdowns of each part: YouTube and Instagram as richardilottux, "Design system in a minute".