Back to notes

UX/UI Design

Design Systems

A start to finish guide to building a design system that survives a real product team: foundations, tokens and the pipeline that ships them, library structure, component APIs, states, accessibility, release process, governance, adoption and measurement. Written for the designer who owns the system or is about to.

Muhammad Saud Musaddiq14 min read

What a System Is

A design system is a product, not a file. Five things separate the two: a backlog you triage on a schedule, releases you announce, a support channel with a stated reply time, a deprecation policy and a named owner with hours allocated. If any one is missing you have a Figma library with good intentions, and it degrades the day its enthusiast changes teams. Judge the system by converted screens, never by components published.

CoversSystem as productNamed ownerSupport SLADeprecation policyJudged by screens
A status card and five obligation rows: backlog triaged every Monday, v3.2.0 shipped with notes, a support channel replying within two working days, three pending deprecations and a named owner funded two days a week, footed by 68% coverage.

The Audit and the Consolidation

Before building anything, inventory what exists: every button, input, colour and type style across the product, with a count of near duplicates. That count is your baseline and the number you report against later. Consolidate with a written rule, keep the variant with the most usage or the best accessibility, and record every kill so nobody reintroduces it. Most teams find 30 to 60 button variants; the system ships with four.

CoversInventoryDuplicate countBaseline metricWritten kills
A funnel from a wall of mismatched save buttons: 42 variants found, narrowed by one written rule of most used and best contrast, down to the four that ship, primary, secondary, tertiary and destructive.

Buy, Extend or Build

Extend an open base such as Radix, shadcn, Material or Ant unless a component is a genuine differentiator. Base kits give you keyboard behaviour, focus management and screen reader semantics for free, which take a team months to get right. Build custom only where the product wins or loses, usually two or three components. The blocking test: if the base component prevents a real user task, build; if it just looks different, theme.

CoversExtend a baseFirst twelveBuild only differentiatorsBlocking test
Three budget columns with counts: buy 12 components as shipped, extend about 80% of the library with your tokens, build only two or three differentiators, under one test asking whether the base blocks a real user task.

Colour and Type

Build colour as ramps with measured lightness steps so that step 600 on any hue passes text contrast on step 50. Publish the allowed pairs, not just the palette, and mark which steps are for surfaces, borders, icons and text. Type is one scale, usually eight sizes from 12 to 48 with a 1.2 to 1.25 ratio, a 16 px body floor on the web and line heights fixed per size, not per use.

CoversMeasured lightnessPublished pairs16px floorOne type scale
A ten step colour ramp beside a type scale: steps 50 to 900 tagged surface, border, icon or text, two pairs marked 8.9:1 published and 1.8:1 not allowed, and sizes from 12 caption to 48 display.

Space and Layout

Spacing is a scale on an 8 pt base with a 4 for fine cases: 4, 8, 12, 16, 24, 32, 48, 64. Ship layout primitives, Stack, Inline, Grid and Container, so screens compose from tokens rather than hand set margins. Name breakpoints by device class, not pixel, and let the container own the gap between children so a component never carries outside margin.

Covers8pt gridLayout primitivesNamed breakpointsContainer owns gap
A measured stack, a space scale and a breakpoint row: padding 24 and gap 16 owned by the container, seven steps from space/4 to space/48, and four breakpoints named by device class from sm 640 to xl 1536.

Surface and Motion

Radius is a five step scale from 4 to 24 plus full, mapped to component size so a chip and a card do not share a corner. Elevation is three bands, resting, raised and overlay, each a paired shadow and surface token so dark mode swaps both. Motion is three durations, 100, 200 and 300 ms, two easings, and every animation respects reduced motion by falling back to a crossfade or nothing.

CoversRadius scaleElevation bandsThree durationsReduced motion
Three specimen rows: six radius steps from xs 4 to full, three elevation bands from a 1px border up to 0 12 32 at 14%, and durations of 100, 200 and 300 ms mapped to hover, menus and modals.

Icons and Assets

Icons live on one grid, usually 24 with a 20 live area, and ship in a fixed size set of 16, 20 and 24, drawn per size rather than scaled. Name by function, not shape: close, not x. The icon component is one component with a swappable glyph, colour bound to the current text colour, so a button never carries a hardcoded icon fill. Illustrations and logos get the same rules with their own grid.

CoversOne gridFunction namesFixed size setSwappable glyph
An icon anatomy, a size set and a naming list: the 24 grid with its 20 live area and 2 pad, three sizes at stroke 1.5, 1.75 and 2, and close over x, delete over trash, warning over triangle.

Token Architecture

Tokens run in three tiers with one direction of reference. Primitives hold raw values and reference nothing. Semantic tokens name roles, surface-danger, text-muted, and reference primitives only. Component tokens are optional and reference semantic only. Product files bind to semantic, never primitive, which is what makes a rebrand one change. Store them in DTCG JSON so every tool reads the same file.

CoversThree tiersReference ruleRole namesDTCG format
Three stacked tiers with arrows running one way only: primitive blue/600 at #2563EB referencing nothing, semantic surface/brand pointing at it, optional component button/primary/bg pointing at that, and the product file binding the semantic tier.

Theming and Modes

Modes live on collections, so collection architecture is theme architecture. Most teams land on four: primitives with no modes, semantic colour with light and dark, a brand collection that overrides a small set of semantic roles per brand, and density with default and compact. Resolution follows the nearest ancestor that sets a mode. In dark mode elevation is lighter surface, not deeper shadow.

CoversMode collectionsResolution orderDark elevationMulti-brand
A four level nesting beside four collections: page on Brand A and dark, section inheriting, card setting light, and the component resolving surface/raised to the Brand A light value, with modes only on semantic colour, brand and density.

Shipping Tokens to Code

Figma variables do not sync to code on their own, so build the pipeline once: export to DTCG JSON on every library publish, transform with Style Dictionary or Tokens Studio into CSS variables, TypeScript and platform formats, and merge through a pull request tagged with the same version as the Figma release. Code consumes semantic names only. When a designer changes surface-brand, the CSS variable changes with the next merge and nobody has a handoff meeting.

CoversOne sourceGenerated outputsPackage versioningSemantic only
A five stage pipeline with code samples: the Figma variable surface/brand exported to tokens.json on publish, transformed by Style Dictionary into CSS, TS, iOS and Android outputs, and merged as a pull request tagged v3.2.0.

Library Structure

Split into foundations, components and patterns files that depend in that order and never backwards. Inside a file, page order is the navigation: cover, changelog, then components alphabetically, with a private page for work in progress. Publish rights sit with two people, and every publish carries a description that becomes the release note.

CoversFile splitDependency orderPage orderPublish rights
Three files depending forward only, foundations then components then patterns, beside the page order inside one of them: cover, changelog, six components alphabetically, a private WIP page, and a publish dialog whose description becomes the release note, with two named publishers on it.

Component APIs

Design the API before the visuals. Variants are for mutually exclusive choices, booleans for presence, text properties for content and slots for anything the consumer composes. Keep one base component per family, so Button, IconButton and LinkButton share tokens and states. If a prop name would confuse an engineer reading the code, rename it in Figma first, the names should match.

CoversProps firstVariants vs booleansSlotsOne base
A Button properties panel beside four mapping rows: variant for mutually exclusive hierarchy and size, boolean for the presence of icons, text for the label, and slot for children, with the Figma names matching the code props exactly.

States and Interaction

Every control ships six states: default, hover, focus visible, active, disabled and loading, plus read only where data is shown but not editable. Focus is a visible ring, never removed, drawn outside the element so it survives any background. Loading keeps the width of the resting state so layout does not jump. Error and success are content states and belong to the field, not the control.

CoversSix statesfocus-visibleRead-onlyLoading width
Six specimens of one Continue button plus a read only field: default, hover, focus visible, active, disabled and loading all held at the same resting width, then a value that is shown but cannot be edited.

Accessibility and Localisation

Accessibility is built into components, not audited in afterwards. Every interactive element meets 24 px minimum target on desktop and 44 on touch, has a name, a role and a focus model documented in the component page, and passes 4.5 to 1 contrast in every mode. Localisation means logical spacing, start and end rather than left and right, strings that can grow 30 percent, and mirrored icons where direction has meaning.

CoversBuilt inTarget sizeFocus modelLogical spacingBidi
One delete button annotated with its 44 px touch target, role, accessible name, 2 px outside focus ring and 4.6:1 contrast, then localisation rows: logical padding-inline-start over padding-left, a Save label growing 30% in German, and mirrored directional icons.

Patterns, Not Just Components

A pattern encodes a decision that components alone cannot: when a form validates, how a destructive action is confirmed, how a table handles selection and empty states. Document each as a recipe, the components used, the order, the copy and the failure paths, and ship it as a Figma page with a working example. Patterns are where product teams stop reinventing and where consistency actually appears to users.

CoversForm validationDestructive confirmTable patternsRecipes
Two worked recipes: an email field validating on blur rather than on keystroke, and a delete confirmation that names the project and counts the 14 files it removes, with cancel on the left, over the four parts every recipe documents.

Documentation and Copy

Each component page answers four questions in order: what it is for, when not to use it, how it behaves and what it looks like. Anti patterns with a screenshot teach more than guidelines. Component descriptions in Figma are the documentation engineers actually see, so write them there first. UX copy rules belong to the system too: error message structure, button label voice and empty state templates.

CoversUsage rulesAnti-patternsComponent descriptionError copy
A toast documentation page with an anti pattern below: four questions in order, purpose, when not to use it, behaviour at four seconds with pause on hover, and anatomy, then the Figma description engineers read in Dev Mode.

Dev Mode and Code Connect

Map every published component to its code counterpart with Code Connect so Dev Mode shows the real import and the real props instead of generated CSS. Variant names map to prop values, boolean properties to boolean props, slots to children. When the mapping exists, an engineer inspecting a design copies working code, and drift between library and codebase becomes visible in the diff instead of in production.

CoversCode ConnectDev ModeProp mappingSingle truth
One button shown twice, as Figma properties and as JSX: hierarchy, size, trailing icon and label line up prop for prop, with variant to prop value, boolean to boolean and slot to children named below.

Releases, Deprecation and Guardrails

Version the library semantically: patch for fixes, minor for additions, major for breaking API changes. Every release ships notes with a migration line per breaking change. Deprecate with a named replacement, a visible badge in Figma, a console warning in code and a removal date two minors away. Guardrails run in CI: a lint that fails on raw hex values, untokenised spacing or a detached instance in a product file.

CoversSemantic versionsRelease notesDeprecation pathLint and CI
A release note above CI lint output: v3.2.0 minor with one addition, one change and one deprecation naming its removal version, then three failures on a raw hex, a 13 px margin and detached instances, and 41 of 41 contrast pairs passing.

Ownership and Contribution

Pick an operating model and say it out loud: centralised team, federated contributors with a core, or a hybrid. Each needs funded time, a request path and a triage cadence, weekly is the minimum that keeps trust. Contribution is a documented path with a template, a review checklist and a stated turnaround. A system with no contribution path accumulates local forks, which are the beginning of the end.

CoversOperating modelStaffingContribution pathTriage cadence
Four operating models above a contribution path: centralised, federated, hybrid where most teams land, and solitary which ends when the enthusiast leaves, each with its trade named, then request, weekly triage, reviewed build and published notes.

Adoption and Migration

Adopt one surface at a time, starting with the screen most teams touch, and publish a before and after with the numbers. The second team is the hardest to win, so pair with them rather than handing over docs. Do not mandate before three teams have adopted voluntarily; a mandate before that produces compliance theatre, detached instances and a system nobody defends.

CoversOne surfaceBefore and afterSecond teamNo early mandate
A before and after of one settings screen, 11 button styles cut to 2 at full coverage, and the rollout under it: team one paired and measured, team two the hardest, team three voluntary, mandate only after all three.

AI and the System

Generated UI is now a consumer of the system. Expose the library through Figma MCP or a design system package so code assistants pull real components and tokens instead of inventing them, and run the same token lint on generated code as on human code. The documentation site is the context the model reads, so keep component descriptions, do and do not rules and prop names current. A system that a model can use correctly is one that humans can too.

CoversMCP and Dev ModeGenerated UIToken lintDocs as context
A prompt, its generated file and a source list: a settings form producing JSX that imports Field and Button from the real library, fed by Figma MCP and the docs, then passing token lint with zero raw values.

Measuring and Failure Modes

Track three numbers: coverage, the share of production screens built from the system; detachment rate, instances detached per week; and time to first screen for a new team. Failure shows early as rising detachments, a growing private page, a changelog that stops, or a release that breaks without notes. Any two together mean the system is losing and the fix is ownership, not more components.

CoversCoverageDetachment rateTime to first screenEarly warnings
Three health readings above four early warnings: 68% coverage, 12 detachments a week and three days to a first screen, then the warnings that count as failure when any two appear together, with ownership named as the fix.