cf.completefrontend
My progress
FUNDAMENTALS · MODULE 17

Design systems

A shared set of design tokens, accessible components, patterns, docs, and rules that many teams build on, so a company ships one consistent product instead of many slightly different ones, and can change its look in one place.

The problem: six teams, fourteen blues

Picture a company with one web app and six product teams: checkout, search, account, admin, help center, and marketing. Each team ships every week. Each needs buttons, inputs, dialogs, tables, and toasts. Nobody planned to build these six times, but each team needed a button on a Tuesday and the quickest path was to write one.

Two years later, someone runs a script over the CSS and finds 14 different blues, 9 button styles, 5 date pickers, and 3 modal dialogs. Two of the dialogs do not trap focus, so keyboard users get lost behind them. One of the date pickers cannot be used with a screen reader at all. Every one of these was a reasonable choice when it was made.

Then the product lead asks for dark mode. In this codebase that means finding every hard-coded #1a73e8 and #ffffff in hundreds of files and deciding what each becomes in dark mode, six times in six styles. A week of work becomes a quarter.

Before and after. Before: six teams, checkout, search, account, admin, help, and marketing, each built its own button with its own blue: 1a73e8, 1f6fd0, 2563eb, 1d4ed8, 0b57d0, and 3367d6. After: one system Button that reads the token color.action.primary, used by all six teams, so a change to the token reaches all six at once.before: 6 teams, 6 buttonscheckout#1a73e8search#1f6fd0account#2563ebadmin#1d4ed8help#0b57d0marketing#3367d6after: one sourceButtoncolor.action.primarycheckoutsearchaccountadminhelpmarketingused by all
On the left, dark mode or a new brand blue means six separate edits in six styles. On the right, it is one token change.

What goes wrong without a shared system

  • Inconsistency. The same action looks different on different pages, and the product feels stitched together.
  • Repeated work. Six teams each spend days on a dropdown. Each fixes its own bugs, and nobody shares the fixes.
  • Accessibility gaps. Focus traps, keyboard support, and ARIA roles are hard to get right. When every team writes its own dialog, some get it wrong, and you find out from an audit or a complaint.
  • Slow global changes. A rebrand, a new font, dark mode, or a higher contrast ratio means touching every screen by hand.
  • Design and code drift. The design file says one thing, the code says another, and nobody knows which one is right.

What a design system actually is

A design system is the shared set of decisions, and the tools that carry them, that lets many teams build one consistent product. It is more than a component library. A good one has five parts that work together.

  1. Design tokens: named values for color, spacing, type, radius, shadow, and motion, like color.action.primary or space.4. They are the single source of truth for the look.
  2. Components: coded, tested, accessible building blocks like Button, TextField, and Dialog, in the frameworks the company uses.
  3. Patterns: agreed ways to solve common problems with those components, like how a form shows errors or what an empty table says.
  4. Documentation: when to use what, live examples, do and don't, accessibility notes, and a changelog.
  5. People and process: a team or group that owns the system, a way for others to contribute, and a release process with versions.

Leave out the tokens and theming becomes a search-and-replace job. Leave out the docs and people use components wrongly. Leave out the process and the system either freezes or falls apart. This lesson covers each part, with the most depth on tokens and theming, where the engineering is least obvious.

Two related lessons go deeper on parts we only touch here: component API design (how to shape props, and codemods) and contract and visual tests (Storybook and visual baselines).

Check your understanding · Exercise 1

A team publishes an npm package with 40 React components and nothing else. Which parts of a design system are missing? Select all that apply.

The mental model: layers of decisions

Think of a design system as a stack of layers. Each layer is built only from the layers below it, and makes a few more decisions than they do. Change a lower layer and everything above it changes with it. That one property is the whole point.

Five stacked layers, widest at the bottom. Tokens: color.text.default, space.4, radius.md. Primitives: Button, TextField, Stack, Text. Patterns: form field, dialog, data table. Templates: app shell, list and detail. Product pages at the top: checkout, search, settings. An arrow on the left points up from shared by everyone to product-specific.product-specificProduct pagescheckout, search, settingsTemplatesapp shell, list and detailPatternsform field, dialog, data tablePrimitivesButton, TextField, Stack, TextTokenscolor.text.default, space.4, radius.mdshared by everyone
Each layer is built only from the layers below it. The bottom changes rarely and reaches every screen; the top changes daily and belongs to product teams.
  1. Tokens at the bottom: raw values with names. blue.600 = #1f5fd1, space.4 = 16px, radius.md = 6px. No components, no layout, just the palette and the scales.
  2. Primitives: the smallest components. Box, Stack, Text, Icon, Button, Input, Checkbox. They read tokens and know nothing about any product.
  3. Patterns (sometimes called composites): primitives put together to solve a common problem. A form field with label, hint, and error. A dialog with a title, body, and actions. A data table with sorting and an empty state.
  4. Templates: page-level layouts. A settings page with a side nav, a list-detail page, a sign-in page. They define regions, not content.
  5. Product pages at the top: owned by product teams, not by the system. They fill templates with real data and product logic.

An everyday comparison is a kitchen. Tokens are the pantry ingredients, primitives are prepared basics like a sauce, patterns are dishes on the menu, and templates are the set menu of starter, main, and dessert. The product page is tonight's dinner. Swap the salt in the pantry for a low-sodium one and every dish changes, without anyone rewriting a recipe.

Two rules that keep the stack healthy

Rule one: depend only downwards. A Button may use tokens. It must not import the checkout page's code. A product page may use templates, patterns, primitives, and tokens, but should not skip them for raw hex values. Code that hard-codes a value is code a global change can no longer reach.

Rule two: the higher the layer, the less of it belongs in the shared system. Tokens and primitives are almost always shared. Patterns are shared when several products need the same one. Templates are shared only in companies with many similar apps. Product pages are never shared.

Lower layers change rarely but each change reaches everything above.
LayerWho owns itChanges how oftenExample of a change
TokensSystem team with designRarely, carefullyNew brand color, darker text for contrast
PrimitivesSystem teamMonthlyButton gets a loading state
PatternsSystem team or contributing teamsMonthly to weeklyForm errors move from top to inline
TemplatesShared or platform teamOccasionallySettings page gets a sticky save bar
Product pagesProduct teamsDailyCheckout adds a gift option

One source of truth, many outputs

The second half of the mental model is where the truth lives. A mature system keeps its decisions in one place, usually token files in a repository, and generates everything else: CSS variables for the web, Swift constants for iOS, XML or Kotlin resources for Android, and variables in the design tool. Designers and engineers then read the same names.

When the truth lives in two places, such as a Figma file and a CSS file that someone copies values between, they drift within months. The fix is not more discipline but a pipeline: edit once, build, and every platform gets the new value. The next chapter shows that pipeline.

Check your understanding · Exercise 2

Which of these belong in the shared design system rather than in a product team's code? Select all that apply.

Design tokens: primitive, semantic, component

A design token is a named design decision: a name plus a value, like space.4 = 16px. Code and design files refer to the name, never to the raw value. That indirection is what makes global change cheap: change the value once and everything that uses the name follows.

Not all tokens are the same kind of decision, though. Most mature systems split them into three tiers, and the split is the most useful idea in this lesson.

The three tiers

  • Primitive tokens (also called base, global, or reference tokens) name every value you are allowed to use. color.blue.600 = #1f5fd1, color.gray.900 = #16181d, space.4 = 16px. They say what a value is, not where it goes. Product code should almost never use them directly.
  • Semantic tokens (also called alias or system tokens) name a purpose and point at a primitive. color.text.default points to color.gray.900, color.action.primary points to color.blue.600, color.border.focus points to color.blue.500. This is the layer product code and components use.
  • Component tokens name one property of one component and point at a semantic token. button.primary.bg points to color.action.primary. They let one component be tuned without touching others.
A graph of token aliases, color prefix left out. Primitives: blue.600 1f5fd1, blue.300 8fb0f0, gray.900 16181d, gray.50 f6f7f9. Semantic action.primary points to blue.600 in light and blue.300 in dark. Semantic text.default points to gray.900 in light and gray.50 in dark. On the right, button.bg and link.color point to action.primary, and the Text component uses text.default.primitivesemanticused byblue.600#1f5fd1blue.300#8fb0f0gray.900#16181dgray.50#f6f7f9action.primarytext.defaultbutton.bglink.colorTextlight mappingdark mapping
A theme moves only the arrows between the semantic and primitive columns. The components on the right never change; a rebrand changes the values in the left column instead.

The tiers pay off when a theme changes. In dark mode, color.text.default points at gray.50 instead of gray.900, and no component or primitive changes: only the arrows in the middle tier move. A brand refresh is the opposite: the primitive blue.600 gets a new hex value, and everything that points at it follows.

Naming tokens

Names are the API of the system, and renaming one later is a breaking change. Most systems use a fixed order from general to specific, such as category, then purpose, then variant, then state.

text
color.text.default
color.text.muted
color.text.danger
color.bg.surface
color.bg.surface.raised
color.border.focus
color.action.primary
color.action.primary.hover
space.1  space.2  space.3  space.4  space.6  space.8
radius.sm  radius.md  radius.full
font.size.body  font.size.heading.lg
motion.duration.fast
Category first, then purpose, variant, and state. Purpose words like text, bg, and danger survive a rebrand; color words like red do not.
  • Name the purpose, not the look. color.text.danger stays right when danger becomes orange. color.red becomes a lie.
  • Use a scale, not adjectives, for sizes. space.1 to space.12 on a 4px grid leaves room to insert values. small, medium, large runs out at extra-extra-large.
  • Keep the set small. If two tokens always change together, they are one token.

Storing tokens: the DTCG format

Tokens live in data files, not in CSS, so that every platform can read them. The W3C Design Tokens Community Group (DTCG) defines a JSON format for this. It is a community group report, not a W3C standard, but it is the format design tools and build tools have converged on. In October 2025 the group published its first stable version, the Design Tokens Format Module 2025.10.

json
{
  "color": {
    "$type": "color",
    "blue": {
      "600": { "$value": "#1f5fd1" },
      "300": { "$value": "#8fb0f0" }
    },
    "action": {
      "primary": {
        "$value": "{color.blue.600}",
        "$description": "Main call to action: buttons, links"
      }
    }
  }
}
Keys starting with $ are format fields; curly braces make an alias. Simplified: the stable spec also defines a structured color value with an explicit color space, and tools differ in which forms they accept.

A build tool then turns the JSON into code for each platform. Style Dictionary, an open-source tool first released by Amazon, is the most common, and version 4 and later read the DTCG format. Plugins such as Tokens Studio sync tokens with Figma. The build resolves aliases, converts units (px to rem for web, points for iOS), and writes one output per platform.

A token pipeline. A design tool syncs through a plugin into token files in DTCG JSON. A build step, Style Dictionary, reads them, resolves aliases, converts units, and writes four outputs: CSS variables for the web, Swift for iOS, Kotlin or XML for Android, and TypeScript types plus the docs site.Design toolsync plugintokensDTCG JSONBuildStyle Dictionaryresolves aliases,converts unitsCSS variableswebSwiftiOSKotlin, XMLAndroidTS and docstypes, docs site
Edit once, build, and every platform gets the same names and values. Nobody copies hex codes between tools by hand.
css
:root {
  --color-white: #ffffff;
  --color-blue-300: #8fb0f0;
  --color-blue-600: #1f5fd1;
  --color-gray-50: #f6f7f9;
  --color-gray-900: #16181d;
  --color-gray-950: #0b0c0f;
  --color-text-default: var(--color-gray-900);
  --color-action-primary: var(--color-blue-600);
  --space-2: 0.5rem;
  --space-4: 1rem;
}

.button-primary {
  background: var(--color-action-primary);
  padding: var(--space-2) var(--space-4);
}
Generated web output. Keeping semantic variables as var() references, instead of resolved hex values, lets a theme re-point them at run time.

Do you need all three tiers?

Decision How many token tiers should a new design system start with?

Primitives only

Components and products use blue.600 and space.4 directly.

Pros

  • Simplest to build and explain.
  • Fine for one product with one theme.

Cons

  • Dark mode means editing every component.
  • Nothing says which blue is for links and which is for info badges.

Primitives plus semantic

Components use only semantic tokens.

Themes re-map semantic tokens to different primitives.

Pros

  • Dark mode and high contrast become a mapping file.
  • Names carry intent, so reviews catch misuse.

Cons

  • Someone must curate the semantic list and say no to one-off tokens.

All three tiers

Each component has its own tokens that alias semantic ones.

Pros

  • One component can be re-tuned per brand without side effects.
  • Useful for white-label products with many brands.

Cons

  • Token count grows fast, often to thousands.
  • Harder to know which token to change for a given fix.

What we pick: Start with primitives plus semantic. Add component tokens only for the few components that genuinely differ per brand or theme, and only when a real request needs them.

Now try it. The lab below has a small app drawn twice, light and dark. Some parts use semantic tokens, and some use hard-coded values copied from the design file. Change a token and watch which parts follow.

Token
Light points to
Dark points to

light: 0 parts changed, 0 hard-coded parts wrong; dark: 0 parts changed, 2 hard-coded parts wrong

The same six-part app in the light and dark themes. light theme: Button: blue.600, Link: blue.600, Body text: gray.900, Card: white, Promo banner: #1f5fd1, Footer text: #16181d. dark theme: Button: blue.300, Link: blue.300, Body text: gray.50, Card: gray.950, Promo banner: #1f5fd1, Footer text: #16181d. light: 0 parts changed, 0 hard-coded parts wrong; dark: 0 parts changed, 2 hard-coded parts wrong.light themeButton: blue.600Link: blue.600Body text: gray.900Card: whitePromo banner: #1f5fd1Footer text: #16181ddark themeButton: blue.300Link: blue.300Body text: gray.50Card: gray.950Promo banner: #1f5fd1Footer text: #16181dfollowed the tokenhard-coded, now wrong
Light: 0 parts changed and 0 hard-coded parts are wrong. Dark: 0 changed and 2 wrong. No component code was edited; hard-coded hex values never follow a token or a theme.

Check your understanding · Exercise 3

A designer wants every link and primary button to become a slightly different blue, in light mode only. What is the smallest correct change in a two-tier system? Select all that apply.

Theming and dark mode

A theme is a mapping from semantic tokens to primitive values. Light and dark are two mappings; high contrast and a second brand are two more. Components never know which theme is active: they read --color-text-default and get whatever the current mapping says.

CSS custom properties make themes cheap

On the web, the mapping is a block of CSS custom properties (CSS variables). Custom properties inherit down the DOM tree like color does, and var() is resolved at the time styles are computed. So switching a theme is one attribute change on an ancestor element: the browser recomputes styles and every component picks up the new values. No component re-renders in React or any other framework.

css
:root,
[data-theme="light"] {
  color-scheme: light;
  --color-bg-surface: var(--color-white);
  --color-text-default: var(--color-gray-900);
  --color-action-primary: var(--color-blue-600);
}

[data-theme="dark"] {
  color-scheme: dark;
  --color-bg-surface: var(--color-gray-950);
  --color-text-default: var(--color-gray-50);
  --color-action-primary: var(--color-blue-300);
}
Two mappings of the same semantic names. Because it is a selector, a theme can also apply to one subtree, such as a dark promo banner inside a light page.

The color-scheme property tells the browser which schemes this part of the page supports, so form controls, scrollbars, and the default page background render to match. Without it you get a dark page with bright white native checkboxes and scrollbars.

Following the operating system

The prefers-color-scheme media query reports the user's operating system setting. Most products offer three choices: Light, Dark, and System. System means "follow the media query, and keep following it if it changes", for example when a phone switches to dark at sunset.

A state machine for the theme preference. Light, stored as theme=light, and Dark, stored as theme=dark, switch to each other when the user picks the other. Either can move to System when the user picks System. System stores nothing and follows prefers-color-scheme: it sits in resolved light or resolved dark and moves between them whenever the operating system changes, for example at sunset.Lightstored: lightDarkstored: darkpick Darkpick Lightpick Systempick SystemSystem: nothing storedresolved lightresolved darkOS setting changes
Only Light and Dark need storage. System is pure CSS: a prefers-color-scheme media query keeps it right, even when the OS switches while the page is open.

The newer light-dark() CSS function puts both values in one place: color: light-dark(#16181d, #f6f7f9) uses the first value when the element's used color scheme is light and the second when it is dark. It needs color-scheme to be set, and it only works for color values. It has been Baseline, supported in all major browsers, since May 2024. It suits a small system with exactly two schemes; a token build that writes separate mappings scales better to high contrast and extra brands.

css
/* Follow the OS by default */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    color-scheme: dark;
    --color-text-default: var(--color-gray-50);
  }
}

/* Or, with light-dark(), one declaration per token */
:root {
  color-scheme: light dark;
  --color-text-default: light-dark(var(--color-gray-900), var(--color-gray-50));
}
Two ways to follow the OS. The :not([data-theme]) guard lets an explicit user choice win over the OS setting.

The flash of the wrong theme

Almost every team ships this bug once. The user picks Dark, and the app saves theme=dark in localStorage. On the next visit the server renders HTML without knowing that, so the page paints light.

Then JavaScript loads, reads localStorage, and switches to dark. The user sees a white flash on every page load, which in a dark room is worse than no dark mode at all.

A sequence diagram between the screen, the browser, and the server. The browser requests the page; the server returns HTML with no theme because it cannot see localStorage. The browser paints the first frame in light. It then requests app.js, about 300 KB, and receives it. Only after hydration does the app read localStorage, find dark, and repaint in dark. The user sees a light page in between.ScreenBrowserServerGET /HTML, no themepaint: lightGET app.js300 KB laterread storage: darklight page shows until the bundle runsfix: theme in the HTML or a head script
The theme arrives last, after the bundle. Anything that runs before the first paint, a cookie read on the server or a tiny script in the head, removes the flash.

Decision Where should the user's theme choice live so the first paint is right?

Cookie, read on the server

Save the choice in a small cookie like theme=dark.

The server renders data-theme="dark" on the html element.

Pros

  • First paint is correct with no extra script.
  • Works for server rendering and streaming.

Cons

  • Pages cached by a CDN must vary on the cookie, or the cache serves one user's theme to another.
  • Fully static pages cannot read it.

Small blocking script in the head

An inline script before any stylesheet or body content reads localStorage and sets the attribute.

Pros

  • Works for static and CDN-cached pages.
  • Tiny: a few hundred bytes.

Cons

  • It blocks rendering by design, and a strict Content Security Policy needs a nonce or hash for it.
  • Frameworks may warn about a hydration mismatch on the html element.

CSS only, for the System choice

When the user has not chosen, send no attribute and let prefers-color-scheme decide.

Pros

  • No script, no cookie, never wrong for the default case.

Cons

  • Cannot remember an explicit choice that differs from the OS on its own.

What we pick: Combine them: CSS media queries for System, plus a cookie when you server-render, or a tiny head script when pages are static or CDN-cached. Never wait for the app bundle to apply the theme.

html
<head>
  <meta name="color-scheme" content="light dark">
  <script>
    try {
      const t = localStorage.getItem("theme");
      if (t === "light" || t === "dark") {
        document.documentElement.dataset.theme = t;
      }
    } catch {}
  </script>
  <link rel="stylesheet" href="/app.css">
</head>
The head-script approach. It runs before the first paint, so there is nothing to flash. The try block covers storage being blocked.

Dark mode is a design, not an inversion

  • Re-check contrast. In our palette blue.600 is 5.8:1 on white but only 3.4:1 on gray.950, below the 4.5:1 that text needs. Each theme mapping needs its own contrast check, which is why dark mode uses lighter steps like blue.300.
  • Show depth with lighter surfaces. Shadows barely show on dark backgrounds, so raised surfaces (cards, menus) usually get a slightly lighter background instead. That is a semantic token like color.bg.surface.raised.
  • Avoid pure black and pure white. Pure white text on pure black can cause glare for some readers, so most dark themes use a dark gray background and off-white text.
  • Watch images and charts. Logos with dark text, screenshots, and chart colors may need dark variants or a token of their own.

The same machinery serves other themes. prefers-contrast: more can switch to a high-contrast mapping. forced-colors: active means the OS (for example Windows contrast themes) replaces your colors, so components must keep borders and focus outlines visible rather than relying on background color alone. Density themes (compact, comfortable) re-map spacing tokens the same way color themes re-map colors.

Check your understanding · Exercise 4

A server-rendered app saves the theme in localStorage and applies it in a useEffect. Users see a white flash. Which fixes remove it? Select all that apply.

Primitives, patterns, and templates

Tokens decide how things look. Components decide how things behave. The hardest everyday question for a system team is not how to build a component but which layer a proposed component belongs in, or whether it belongs in the system at all.

Primitives: small, boring, everywhere

Primitives are the components every screen uses: layout helpers (Box, Stack, Grid), typography (Text, Heading), and basic controls (Button, Link, TextField, Checkbox, Select, Icon). They take a small set of props tied to tokens, such as size="sm" or gap="4", and refuse arbitrary values. That refusal is the feature: a Stack that accepts gap="13px" no longer enforces the spacing scale.

Primitives should render semantic HTML. A Button renders a button element, a Link renders an a element with an href. This sounds obvious, yet a clickable div posing as a button is a common source of keyboard and screen reader bugs.

Patterns: solving a problem once

A pattern combines primitives to solve a recurring problem, and it encodes a decision that would otherwise be made differently by every team. A form field pattern decides where the label goes, how hint text and error text are announced, and when validation runs. A dialog pattern decides how focus moves in, where it goes on close, and what Escape does.

  • Form field: label, control, hint, and error, wired with for, id, and aria-describedby so a screen reader reads the error with the field.
  • Dialog: focus moves inside on open, stays inside, and returns to the trigger on close; Escape closes it; the page behind is inert.
  • Data table: sortable headers announce the sort direction; an empty state says what to do next; a loading state keeps the layout from jumping.
  • Toast: announced through a live region without stealing focus, and stays long enough to read.

Patterns can live as code components, as documented recipes, or both. A recipe ("a destructive confirmation is a Dialog with the danger button on the right and Cancel focused first") is cheaper to maintain than a component with 20 props. Start with a recipe once three or more teams need the pattern, and promote it to a component when its API has settled.

Templates: layout, not content

A template is a page skeleton: an app shell with header and side navigation, a list and detail layout, a settings page with sections and a save bar. Templates are worth sharing in companies with many internal tools, where every admin page looks the same. In a consumer product with a handful of unique pages, they often belong to the product team.

A decision tree for a proposed component. Is it needed by three or more teams? If no, keep it in the product. If yes: does it encode product rules? If yes, it is product code built from system parts. If no: is its API settled after two or three real uses? If no, write a recipe in the docs. If yes, make it a system component.Needed by 3+ teams?noKeep in productyesEncodes product rules?yesProduct codebuilt from partsnoAPI settled?noRecipe in docsyesSystem componenttokens, docs, testsstart
Most proposals stop before the bottom box. A recipe in the docs is a cheap, reversible first step; a component is a promise you have to keep across versions.

Accessibility built in

The biggest single benefit of a design system is that accessibility gets solved once by the people best placed to solve it, then inherited by every team. A product engineer who uses the system's Combobox gets the keyboard model, the aria-expanded and aria-activedescendant wiring, and the screen reader announcements for free. The ARIA patterns lesson covers those widget patterns in detail.

  • Focus styles as tokens. A color.border.focus token and a shared focus ring, shown with :focus-visible, so focus is always visible and consistent.
  • Contrast in the tokens. Every text and background pair that the semantic layer allows is checked against WCAG ratios (4.5:1 for normal text, 3:1 for large text and UI parts) in every theme, in CI.
  • Motion tokens that respect settings. Duration tokens drop to near zero under prefers-reduced-motion: reduce.
  • Required accessible names. The IconButton type makes a label prop required, so a button with only an icon cannot ship without a name.

The design system decides which components exist and what they promise. How people call them (compound components like Tabs.List, headless hooks, controlled and uncontrolled state) is the subject of the component API design lesson.

Check your understanding · Exercise 5

Three teams have each built a "filter bar" above a table: search box, a few dropdowns, and a Clear button. What should the system team do? Select all that apply.

Versioning and breaking changes

A design system is a dependency of every product team. Each release can fix bugs for 30 apps at once, or break 30 apps at once. Versioning is how the system tells its users which one to expect, and how it gives them time to move.

Semantic versioning, applied to UI

Most systems use semantic versioning (semver): a version like 4.2.1 is MAJOR.MINOR.PATCH. A patch fixes bugs, a minor adds things without breaking existing use, and a major may break it. The hard part is deciding what "break" means for UI, because a UI has more surface than its props.

The public API of a design system includes props, tokens, defaults, and anything consumers can reasonably depend on.
ChangeUsuallyWhy
Remove or rename a prop, component, or tokenMajorCode that uses the old name stops compiling or silently loses styling.
Change a default (Button defaults to size="md" instead of "lg")MajorEvery call site that relied on the default changes without a code change.
Change the DOM structure or class namesMajor, if consumers can see itTests that query the DOM and CSS overrides that target inner elements break.
Add an optional prop or a new componentMinorExisting code is unaffected.
Add a token or themeMinorNothing that exists changes.
Change a token value (new brand blue)Minor, announcedThe API is the same, but screenshots and some layouts change. Big visual shifts deserve a major or a flag.
Fix a focus bug or a wrong ARIA attributePatchBehavior moves to what was always promised.

An observation known as Hyrum's Law applies strongly here: with enough users, every behavior someone can observe becomes something someone depends on. If 12 apps style .ds-button__label, renaming that class is a breaking change even if the docs said class names are private. The defense is to not expose internals in the first place: no stable class names, styles applied through tokens and props.

Change
Your guess

Classifying the change: Rename color.primary to color.action.primary. From version 4.2.1 it could go to 5.0.0 major, 4.3.0 minor, or 4.2.2 patch. Pick a guess to see the answer.Rename color.primary to color.action.primary4.2.15.0.0 majorevery app must act4.3.0 minorsafe, new things4.2.2 patchsafe, a fixWhich version should this change ship in?
Pick major, minor, or patch to check your answer.

Recording changes with changesets

In a repository with many packages (tokens, React components, icons, a web components build), tracking what changed and which version each package needs is error-prone by hand. Changesets is a popular tool for this. Each pull request adds a small markdown file that names the packages it changes, the bump type, and a sentence for the changelog. At release time the tool combines them, bumps versions, writes CHANGELOG.md files, and publishes.

markdown
---
"@acme/ds-react": minor
"@acme/ds-tokens": minor
---

Add `Button` `loading` prop to replace `busy`, and the
`color.action.primary.disabled` token. `busy` keeps working
for now and will be removed in 5.0.
A changeset file for the 4.2 release, written in the same pull request as the code. Reviewers see the version impact next to the change.

Deprecation windows

Product teams have their own roadmaps, and nobody drops a feature to upgrade a button. So systems rarely remove anything in one step. They follow a deprecation window: ship the new way next to the old way, warn, help people move, then remove in the next major.

A six-month timeline for replacing the Button prop busy with loading. Version 4.2 at month 0 adds loading; busy still works. Version 4.3 at month 1 adds a development warning and lint rule for busy. Version 4.4 at month 2 ships a codemod. Version 5.0 at month 6 removes busy. Call sites still using busy: 400, 390, 160, 70, 20, 5, then 0.4.24.34.45.0loading propbusy propdev warningcodemodbusy calls400390160702050month 0123456
The warning alone barely moves usage; the codemod does most of the work. By the time 5.0 removes the old prop, almost nobody is still using it.
  1. Add the replacement in a minor release. Old and new both work.
  2. Mark the old one deprecated: a JSDoc @deprecated tag so editors strike it through, a development-only console warning, a lint rule, and a docs note.
  3. Ship a codemod, a script that rewrites call sites automatically. The component API design lesson covers how codemods work.
  4. Measure usage across repositories until it reaches zero or close to it.
  5. Remove in the next major, with a migration guide. A common window is one or two quarters, long enough for teams to plan, short enough that the system does not carry two APIs forever.

Tokens follow the same path. When color.primary becomes color.action.primary, the build keeps color.primary as an alias of the new token, marked deprecated, for one major version. A lint rule flags the old name in product code.

Version skew

In a large company, different apps run different versions of the system. That is normal and fine when each app is its own page. It becomes a problem when two versions end up on the same page, for example with micro-frontends, where one team's widget brings ds-react@4 and the host runs ds-react@5.

  • Two copies of the code in the bundle, often tens of KB or more.
  • Clashing global CSS: both versions define --color-action-primary or the same class names with different values, and whichever loads last wins.
  • Two buttons that look slightly different on the same screen, which is exactly what the system was meant to prevent.

Fixes include sharing one copy at run time (for example a shared singleton in module federation), declaring the system as a peer dependency so the host decides the version, and scoping CSS per major version with a prefix. The cheapest fix is organizational: keep everyone within one major version by making upgrades small and frequent.

Check your understanding · Exercise 6

Which of these changes should go out as a major version? Select all that apply.

Documentation and adoption

A design system that nobody uses is a cost with no benefit. Adoption is not automatic, even inside one company: product teams are busy, their own button already works, and the system's button might not do the one thing they need. Docs, tooling, and measurement are how a system earns its users.

What good docs contain

Docs answer the questions a product engineer has at the moment of building: which component do I need, how do I use it, and what should I not do with it. A props table alone answers only the second question.

  • When to use it, and when not to. "Use Link to navigate, Button to perform an action" prevents more bugs than any props table.
  • Live, editable examples for common cases, so people copy working code.
  • Do and don't pairs with pictures: one primary button per view, not three.
  • Accessibility notes: the keyboard behavior, what screen readers announce, and what the consumer must still provide (a label, a heading level).
  • Props and token reference, generated from the types and token files so it stays current.
  • Changelog and migration guides, linked from each component page.

Many teams build docs on Storybook, which renders each component state as a story and can generate props tables. Others build a dedicated docs site that embeds stories. Either way, stories double as the input for visual regression tests; the contract and visual tests lesson covers that side.

Measuring adoption

Adoption metrics tell the system team where to help, and tell leadership whether the investment is paying off. The useful ones are measured automatically from code, not from surveys.

Track these per team and over time. A single company-wide number hides where help is needed.
MetricHow to measureWhat it tells you
Component coverageParse each repo's JSX and count system components versus raw elements like button, input, and local look-alikesHow much of the UI comes from the system
Hard-coded valuesA stylelint or ESLint rule counts raw hex colors, px spacing, and font sizes outside tokensHow much of the look will not follow a theme change
Version spreadRead package.json and lockfiles across reposHow many teams are on old majors and need upgrade help
Deprecated API usageCount calls to deprecated props or tokensWhether a removal can safely ship
Detached design componentsDesign tool analytics on instances detached from the libraryWhere designers find the library lacking
A bar chart of design system coverage per team: the share of interactive elements that come from the system, with the number of hard-coded colors. checkout 92 percent, 3 raw colors, search 85 percent, 6 raw colors, help 77 percent, 11 raw colors, account 64 percent, 24 raw colors, admin 41 percent, 58 raw colors, marketing 28 percent, 140 raw colors. A dashed line marks an 80 percent target.interactive elements from the system, per teamcheckout92%, 3 raw colorssearch85%, 6 raw colorshelp77%, 11 raw colorsaccount64%, 24 raw colorsadmin41%, 58 raw colorsmarketing28%, 140 raw colorstarget 80%
One company-wide average, about 65 percent here, would hide that admin and marketing need help. Raw color counts track the same story from the styling side.

Coverage is easy to game. A team can wrap every page in a system Box and score high while hand-coding everything inside. Counting interactive elements (buttons, inputs, menus, dialogs) is more honest, because that is where consistency and accessibility matter most.

Driving adoption

  • Make the right thing the easy thing. A starter template that already uses the system and good TypeScript autocomplete do more than any mandate.
  • Lint, then enforce. Start lint rules for raw colors and native elements as warnings, and make them errors for new code once most teams are clean.
  • Help with migrations. Pair with a team to move its first screens. Each migration teaches the system team what is missing.
  • Fix gaps fast. A team that waits two months for a feature builds its own and stops asking. A documented escape hatch, such as a className or slot, beats a fork.
  • Find champions. One engineer per product team who answers questions and brings feedback back.
  • Show the payoff. When dark mode ships in a week for adopted screens and not for the rest, adoption sells itself.

Check your understanding · Exercise 7

Leadership wants one number to show whether the design system is working. Which are good choices? Select all that apply.

Governance: who decides what goes in

Every design system has to answer the same questions: who decides whether a new component goes in, who reviews a token change, and who fixes a bug at 6 pm before a launch? The answers are its governance model. Weak governance sinks more systems than weak technical choices do.

Three models

Decision Who should build and own the design system?

Centralized team

A dedicated team of designers and engineers builds and owns everything.

Product teams are consumers and file requests.

Pros

  • Consistent quality, clear ownership, fast decisions.
  • Deep accessibility and API expertise in one place.

Cons

  • Becomes a bottleneck: the queue grows faster than the team.
  • Can drift away from what products actually need.

Federated

No dedicated team. Designers and engineers from product teams each give part of their time.

Decisions by a council or working group.

Pros

  • Built by people who feel the product pain daily.
  • Cheap to start.

Cons

  • Part-time work slips when product deadlines hit.
  • Harder to keep a coherent API and quality bar.

Hybrid: core team plus contributions

A small core team owns tokens, primitives, tooling, and the quality bar.

Product teams contribute patterns through a defined process, with core review.

Pros

  • Scales beyond the core team's size.
  • Product knowledge flows in; quality stays consistent.

Cons

  • Needs a clear contribution process and reviewers who say yes and no quickly.

What we pick: A single small product can start federated. Once several teams depend on the system, fund a core team and run a hybrid model: the core owns the foundations, and teams contribute the patterns they know best.

A matrix of three governance models against five properties: consistent, fast calls, scales, fits need, and cheap. Centralized: yes, yes, no, some, no. Federated: no, no, some, yes, yes. Hybrid: yes, some, yes, yes, some.consistentfast callsscalesfits needcheapCentralizedFederatedHybridyesyesnosomenononosomeyesyesyessomeyesyessome
No model wins every column. Hybrid gives up a little speed and cost to keep consistency while scaling with the number of teams.

A contribution process that works

  1. Propose: a short issue that names the problem, shows the existing local versions, and lists which teams need it.
  2. Agree the design: designers from both teams agree the behavior, states, tokens, and keyboard and screen reader behavior.
  3. Build: the proposing team builds it in the system repository, with stories and tests.
  4. Review: the core team reviews API, accessibility, and visuals. Clear criteria keep this from becoming taste.
  5. Release and own: the core team takes over ownership, or the contributing team stays listed as co-owner for bugs.

Not every request deserves a new component. Some are better answered with a recipe, a new variant of an existing component, or a documented "build this locally". A clear "no, and here is what to do instead" keeps the system small and still unblocks the team.

Funding and roles

A system serving dozens of engineers needs at least a few people on it full time, usually designers and engineers together, often with an accessibility specialist shared with other teams. Budgets are easier to defend with numbers: hours saved per component not rebuilt, accessibility issues fixed once instead of per team, and time to ship a theme or rebrand.

Think first

A product team needs a Select with grouped options for a launch next week. The system's Select does not support groups, and the core team's queue is three weeks long. What do you do?

In production: failure modes and real systems

Most design systems do not fail on day one. They fail slowly: the tokens sprawl, teams route around the components, the bundle grows, and two years later the company starts a "new design system" project. Knowing the usual failure modes lets you design against them from the start.

What breaks, how you notice, how you fix it

FailureHow you noticeFix
Token sprawl: hundreds of near-duplicate semantic tokensNobody can say which token to use; reviews argue about namesAudit and merge; require a use case for each new semantic token
Hard-coded values creep backDark mode or a rebrand misses screens; lint counts riseLint rule for raw colors and spacing; fail CI for new code
Overrides on internalsUpgrades break screens that styled inner class namesNo stable internal class names; offer props, slots, and documented tokens instead
Specificity fights between system CSS and app CSS!important spreads through product codePut system styles in a lower cascade layer (@layer) so app styles win predictably
Bundle bloatImporting one Button pulls in the whole libraryES modules, sideEffects: false, per-component entry points; check with a bundle analyzer
Bottleneck teamRequest queue grows; local copies appearHybrid governance, escape hatches, faster minor releases
Upgrade fatigueMany apps stuck two majors behindFewer majors, codemods, and upgrade help from the system team

Performance angles

A design system sits on every page, so its costs multiply. JavaScript size comes first: a primitive like Button should cost a few KB at most, and teams should pay only for what they import. Fonts and icons come next: a system that loads four font weights and a 300 KB icon font on every page undoes careful product work.

The third cost is styling runtime. CSS-in-JS libraries that generate styles while rendering add work to every render and complicate server rendering and React Server Components. Several large systems have moved to plain CSS, CSS Modules, or build-time extraction.

Themes themselves are cheap. Changing the theme attribute on the root element triggers one style recalculation for the page, usually a few milliseconds, not something to optimize.

Real systems worth studying

Several large companies publish their systems openly. Details change often, so treat this as a map for further reading, not as current facts to quote.

All four publish their docs openly; reading their token and theming pages is the fastest way to see these ideas at scale.
SystemOwnerWorth noticing
Material Design 3GoogleToken names show the three tiers: reference (md.ref), system (md.sys), and component (md.comp). Dynamic color can build a whole scheme from one source color, such as an Android wallpaper.
PrimerGitHubTokens live in a separate primitives package built with Style Dictionary. GitHub offers themes beyond light and dark, including high contrast and color-blind themes for protanopia, deuteranopia, and tritanopia.
PolarisShopifyBuilt for the Shopify admin and the third-party apps inside it, so it also serves outside developers. In 2025 Shopify made Polaris web components the default and put Polaris React into maintenance mode, so apps can use it from any framework.
CarbonIBMShips React and web components versions from shared tokens, and four built-in themes: White, Gray 10, Gray 90, and Gray 100.

The common thread: each keeps its decisions in tokens and docs, separate from component code. That is what lets one system serve several frameworks and platforms.

Check your understanding · Exercise 8

Product teams keep adding !important to override system component styles. What fixes the cause rather than the symptom? Select all that apply.

Design systems in a system design interview

Design systems come up in two ways. Sometimes the whole question is about one: "design a component library for a company with 20 teams". More often it is a follow-up inside another problem: "how would you keep this consistent across teams?", "how would you add dark mode?", or "how would you roll out a breaking change to the shared table?".

What to draw

  1. The layer stack: tokens, primitives, patterns, templates, products. Say which layers are shared and who owns each.
  2. The token pipeline: one JSON source, a build step, outputs for web, iOS, Android, and the design tool.
  3. The theme mapping: semantic tokens pointing at different primitives for light and dark, applied with CSS custom properties.
  4. The release flow: changesets, semver, deprecation window, codemods, adoption dashboards.

What to say, by level

Inside a larger problem, spend a minute or two on it unless the interviewer leans in. When the whole question is a design system, the people and process half matters as much as the code half.

LevelA strong answer covers
JuniorShared components and tokens instead of hard-coded values; components render semantic HTML and are accessible; dark mode with CSS variables.
MidPrimitive versus semantic tokens; theming without a flash on server-rendered pages; semver and deprecation; Storybook docs and visual tests; lint rules for raw values.
SeniorGovernance model and funding; adoption metrics per team; version skew in micro-frontends; multi-platform token builds; when not to build a component; cost of majors to consumers.

Common mistakes

  • Talking only about the component library. Mid and senior interviewers look for tokens, docs, governance, and adoption.
  • Naming tokens by color. color.blue as the button color makes the dark mode answer fall apart.
  • Applying the theme in a client effect. Say how the first paint gets the right theme.
  • Forgetting the consumers. Every breaking change costs other teams time; mention deprecation windows and codemods.
  • Building everything up front. Start with the components most teams need (button, input, dialog, layout) and grow from real demand.

Check your understanding · Exercise 9

An interviewer asks: "Your company has 15 apps and wants dark mode next quarter. How do you approach it?" Which points belong in a strong answer? Select all that apply.

Summary

  • A design system is tokens, components, patterns, docs, and a process, run as a product whose users are other teams.
  • Think in layers: tokens, primitives, patterns, templates, products. Each layer uses only the one below, so a low change reaches everything above.
  • Split tokens into primitive (what a value is) and semantic (what it is for). Components use semantic tokens; themes re-map them.
  • Keep tokens in one source, such as DTCG-format JSON, and generate CSS variables and native outputs with a build tool like Style Dictionary.
  • Theme with CSS custom properties and color-scheme; follow prefers-color-scheme for System; apply an explicit choice before first paint with a cookie or head script.
  • Build accessibility into primitives and patterns once, so every team inherits it, and check token contrast in every theme.
  • Use semver with a wide view of what breaks (props, tokens, defaults, DOM), ship deprecations with warnings and codemods, and keep majors rare.
  • Measure adoption from code per team, and run a hybrid governance model: a core team for foundations, contributions for patterns.

In the interview

Questions interviewers ask about this. Answer each out loud first, then compare.

Question 1

What is the difference between primitive and semantic tokens, and why does it matter?

Question 2

How do you implement dark mode on a server-rendered site without a flash?

Question 3

How would you ship a breaking change to a component used by 30 apps?

Question 4

How do you know whether a design system is succeeding?

Question 5

Centralized team or federated contributions: how would you organize a design system for 20 product teams?

Practice it

These design problems lean on design systems.

References

Read next