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.
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.
- Design tokens: named values for color, spacing, type, radius, shadow, and motion, like
color.action.primaryorspace.4. They are the single source of truth for the look. - Components: coded, tested, accessible building blocks like
Button,TextField, andDialog, in the frameworks the company uses. - Patterns: agreed ways to solve common problems with those components, like how a form shows errors or what an empty table says.
- Documentation: when to use what, live examples, do and don't, accessibility notes, and a changelog.
- 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.
- 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. - Primitives: the smallest components.
Box,Stack,Text,Icon,Button,Input,Checkbox. They read tokens and know nothing about any product. - 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.
- Templates: page-level layouts. A settings page with a side nav, a list-detail page, a sign-in page. They define regions, not content.
- 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.
| Layer | Who owns it | Changes how often | Example of a change |
|---|---|---|---|
| Tokens | System team with design | Rarely, carefully | New brand color, darker text for contrast |
| Primitives | System team | Monthly | Button gets a loading state |
| Patterns | System team or contributing teams | Monthly to weekly | Form errors move from top to inline |
| Templates | Shared or platform team | Occasionally | Settings page gets a sticky save bar |
| Product pages | Product teams | Daily | Checkout 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.defaultpoints tocolor.gray.900,color.action.primarypoints tocolor.blue.600,color.border.focuspoints tocolor.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.bgpoints tocolor.action.primary. They let one component be tuned without touching others.
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.
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
- Name the purpose, not the look.
color.text.dangerstays right when danger becomes orange.color.redbecomes a lie. - Use a scale, not adjectives, for sizes.
space.1tospace.12on a 4px grid leaves room to insert values.small,medium,largeruns out atextra-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.
{
"color": {
"$type": "color",
"blue": {
"600": { "$value": "#1f5fd1" },
"300": { "$value": "#8fb0f0" }
},
"action": {
"primary": {
"$value": "{color.blue.600}",
"$description": "Main call to action: buttons, links"
}
}
}
}
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.
: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);
}
Do you need all three tiers?
Decision How many token tiers should a new design system start with?
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.
light: 0 parts changed, 0 hard-coded parts wrong; dark: 0 parts changed, 2 hard-coded parts wrong
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.
: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);
}
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.
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.
/* 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));
}
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.
Decision Where should the user's theme choice live so the first paint is right?
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.
<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>
Dark mode is a design, not an inversion
- Re-check contrast. In our palette
blue.600is 5.8:1 on white but only 3.4:1 ongray.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 likeblue.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, andaria-describedbyso 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.
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.focustoken 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
IconButtontype 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.
| Change | Usually | Why |
|---|---|---|
| Remove or rename a prop, component, or token | Major | Code that uses the old name stops compiling or silently loses styling. |
Change a default (Button defaults to size="md" instead of "lg") | Major | Every call site that relied on the default changes without a code change. |
| Change the DOM structure or class names | Major, if consumers can see it | Tests that query the DOM and CSS overrides that target inner elements break. |
| Add an optional prop or a new component | Minor | Existing code is unaffected. |
| Add a token or theme | Minor | Nothing that exists changes. |
| Change a token value (new brand blue) | Minor, announced | The 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 attribute | Patch | Behavior 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.
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.
---
"@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.
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.
- Add the replacement in a minor release. Old and new both work.
- Mark the old one deprecated: a JSDoc
@deprecatedtag so editors strike it through, a development-only console warning, a lint rule, and a docs note. - Ship a codemod, a script that rewrites call sites automatically. The component API design lesson covers how codemods work.
- Measure usage across repositories until it reaches zero or close to it.
- 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-primaryor 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
Linkto navigate,Buttonto 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.
| Metric | How to measure | What it tells you |
|---|---|---|
| Component coverage | Parse each repo's JSX and count system components versus raw elements like button, input, and local look-alikes | How much of the UI comes from the system |
| Hard-coded values | A stylelint or ESLint rule counts raw hex colors, px spacing, and font sizes outside tokens | How much of the look will not follow a theme change |
| Version spread | Read package.json and lockfiles across repos | How many teams are on old majors and need upgrade help |
| Deprecated API usage | Count calls to deprecated props or tokens | Whether a removal can safely ship |
| Detached design components | Design tool analytics on instances detached from the library | Where designers find the library lacking |
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
classNameor 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?
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 contribution process that works
- Propose: a short issue that names the problem, shows the existing local versions, and lists which teams need it.
- Agree the design: designers from both teams agree the behavior, states, tokens, and keyboard and screen reader behavior.
- Build: the proposing team builds it in the system repository, with stories and tests.
- Review: the core team reviews API, accessibility, and visuals. Clear criteria keep this from becoming taste.
- 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?
Unblock the launch without forking the system. If the system exposes a headless or lower-level version, the team builds grouped options on top of it and keeps the system's keyboard and ARIA behavior.
If not, the team builds a local component, marked as temporary, and files a contribution proposal with their version attached.
The core team prioritizes grouped options as a minor release and the local copy is removed when it lands. The adoption metrics show the local copy until then, so it does not quietly become permanent.
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
| Failure | How you notice | Fix |
|---|---|---|
| Token sprawl: hundreds of near-duplicate semantic tokens | Nobody can say which token to use; reviews argue about names | Audit and merge; require a use case for each new semantic token |
| Hard-coded values creep back | Dark mode or a rebrand misses screens; lint counts rise | Lint rule for raw colors and spacing; fail CI for new code |
| Overrides on internals | Upgrades break screens that styled inner class names | No stable internal class names; offer props, slots, and documented tokens instead |
| Specificity fights between system CSS and app CSS | !important spreads through product code | Put system styles in a lower cascade layer (@layer) so app styles win predictably |
| Bundle bloat | Importing one Button pulls in the whole library | ES modules, sideEffects: false, per-component entry points; check with a bundle analyzer |
| Bottleneck team | Request queue grows; local copies appear | Hybrid governance, escape hatches, faster minor releases |
| Upgrade fatigue | Many apps stuck two majors behind | Fewer 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.
| System | Owner | Worth noticing |
|---|---|---|
| Material Design 3 | Token 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. | |
| Primer | GitHub | Tokens 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. |
| Polaris | Shopify | Built 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. |
| Carbon | IBM | Ships 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
- The layer stack: tokens, primitives, patterns, templates, products. Say which layers are shared and who owns each.
- The token pipeline: one JSON source, a build step, outputs for web, iOS, Android, and the design tool.
- The theme mapping: semantic tokens pointing at different primitives for light and dark, applied with CSS custom properties.
- 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.
| Level | A strong answer covers |
|---|---|
| Junior | Shared components and tokens instead of hard-coded values; components render semantic HTML and are accessible; dark mode with CSS variables. |
| Mid | Primitive versus semantic tokens; theming without a flash on server-rendered pages; semver and deprecation; Storybook docs and visual tests; lint rules for raw values. |
| Senior | Governance 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.blueas 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; followprefers-color-schemefor 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?
Primitive tokens name raw values, like color.blue.600 = #1f5fd1 or space.4 = 16px. They say what a value is. Semantic tokens name a purpose, like color.action.primary or color.text.danger, and point at a primitive.
Components and product code use semantic tokens only. Then a theme is just a different mapping: dark mode points color.text.default at a light gray instead of a dark one, and no component changes.
It also helps reviews: color.text.danger on a success message is an obvious mistake, while color.red.600 hides the intent. A third tier, component tokens like button.primary.bg, is useful only when single components need to differ per brand.
Question 2
How do you implement dark mode on a server-rendered site without a flash?
Theme with CSS custom properties: semantic variables re-mapped under [data-theme="dark"], plus color-scheme so native controls match. For users who chose System, a prefers-color-scheme media query handles it with no script.
For an explicit choice, the first paint must already know it. Store it in a cookie and render the attribute on the server, or run a tiny inline script in the head that reads localStorage before the first paint. Never apply it in a client effect after hydration.
If HTML is cached at a CDN, vary the cache on the theme cookie or keep the theme out of the cached HTML and use the head script.
Question 3
How would you ship a breaking change to a component used by 30 apps?
Avoid a hard break. Ship the new API next to the old one in a minor release, mark the old one deprecated with a JSDoc tag, a development-only warning, and a lint rule, and provide a codemod.
Track usage of the deprecated API across repositories and help the teams with the most usage. Remove it in the next major once usage is near zero, with a migration guide, typically after one or two quarters.
Record the change with a tool like changesets so the changelog and version bump are reviewed with the code, and keep majors rare because each one costs every consumer an upgrade.
Question 4
How do you know whether a design system is succeeding?
Measure from code: the share of interactive elements that come from the system, per team; the count of hard-coded colors and spacing values; version spread across apps; and use of deprecated APIs.
Pair it with outcomes: how long a theme or brand change takes, how many accessibility issues are found per audit, and how often teams build local copies (and why).
Read every local copy as feedback. A rising number of local components means the system is missing something or is too slow to respond.
Question 5
Centralized team or federated contributions: how would you organize a design system for 20 product teams?
A hybrid. A small funded core team owns tokens, primitives, tooling, accessibility, and the release process, because those need consistency and deep expertise.
Product teams contribute patterns through a defined process: proposal, joint design review, build in the system repository, core review, release. That scales beyond the core team and brings product knowledge in.
Escape hatches (headless parts, documented slots) keep teams unblocked when the core team is busy, and adoption metrics make temporary local copies visible.
Practice it
These design problems lean on design systems.
References
- Design Tokens Format Module 2025.10 The first stable DTCG format for token files.
- Style Dictionary: DTCG support Build tool that turns token files into platform outputs.
- MDN: prefers-color-scheme
- MDN: color-scheme
- MDN: light-dark()
- Semantic Versioning 2.0.0
- Changesets Versioning and changelogs for multi-package repositories.
- Material Design 3 A public system with three token tiers and dynamic color.
Read next
- Designing component APIsHow to shape a component's props, children, slots, events, and ref so other engineers can use it for cases you didn't predict, without it growing a hundred props: composition, compound components, controlled state, and headless behavior.
- Contract and visual regression testsTests that guard the edges between teams: schema checks and consumer-driven contracts stop an API change that would break a client, and visual tests catch pixel changes nobody meant, from one button to a whole design system.
- ARIA widget patternsThe tested recipes for custom widgets from the ARIA Authoring Practices Guide: combobox, listbox, menu, tabs, dialog, tree, grid, disclosure, and accordion, with the roles, states, and keys each one promises, and how to pick the right one.
- Color, contrast, and motionHow people with low vision, color blindness, motion sensitivity, or custom system themes see your UI, and the rules that keep it usable: contrast ratios, never color alone, reflow at 400% zoom, target size, and the user preference media queries.
- MonoreposOne repository for many apps and shared packages, plus what makes it scale: workspaces that link packages, task runners that build only what changed and cache the rest, folder owners, and a rule for versioning internal code.
- Micro-frontendsSplitting one web app into parts that separate teams build and deploy on their own, then combining them at build time, on the server, or in the browser: how each approach works, what it costs in bytes and complexity, and why most teams should not do it.