Design Systems
Tokens to components to patterns to applications: a versioned product with a public API, accessibility built in at the component layer, and an adoption problem that decides whether it is leverage or a cost centre.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
What is a design system actually made of, and what makes one that teams use rather than one that teams work around?
A person moves between five screens built by five teams. They expect the same control to look the same, behave the same, respond to the same keys and be reachable the same way — because as far as they are concerned it is one product.
Build a component library: put the buttons, inputs and modals in a shared package, publish it, and tell teams to use it instead of writing their own.
A component library without a token layer hardcodes colour and spacing into every component, so the first re-theme is a rewrite of the library rather than a change to a set of values (Design Tokens).
- A component library without a token layer hardcodes colour and spacing into every component, so the first re-theme is a rewrite of the library rather than a change to a set of values (Design Tokens).
- Without a stable public API, every internal refactor is a breaking change for consumers. Teams pin an old version, stop upgrading, and the library forks in practice while looking unified in the repository.
- Without versioning discipline, one product on version 2 and another on version 4 render two different buttons in one screenshot, and the design system is now the source of the inconsistency it was meant to remove.
- Without a real accessibility contract, the shared
Buttonis adivwith anonClick, and the library has industrialised a defect: every product now has the same bug in fifty places (Semantics Are Behaviour). - Without adoption, it is a cost centre with a maintainer. Teams keep writing local components because the shared one does not do the thing they need and the escape hatch does not exist, and the library becomes a tax nobody pays.
- Without patterns above components, every team assembles the same form, empty state and destructive-confirmation flow slightly differently, and the product is consistent at the widget level and incoherent at the task level.
What is actually happening
In the browser, not in the framework.
- A design system is four layers, and each layer consumes only the layer below it. Tokens are named values — colour, space, radius, type scale, motion duration. Components are the interactive primitives that consume tokens. Patterns compose components into recurring solutions: a form, a data table, an empty state, a destructive confirmation. Applications compose patterns into products (Design Tokens).
- The direction of that dependency is the whole architecture. A component may not hardcode a colour; a pattern may not reach past components into raw values; an application may not fork a component to change one detail. Every violation is a future re-theme that does not work.
- A design system is a product with consumers, which means it has a public API, a versioning policy, a deprecation path, a changelog and a support channel. The components are the smallest part of it (What a Component Owes Its Caller).
- Its public API is the props, the slots, the events, the token names and the DOM it renders. If consumers style your internals, your internals are your API whether you intended them to be or not (Composition and Slots).
- Accessibility lives at the component layer because that is where it is cheapest and most durable: semantics, keyboard behaviour, focus management and announcement written once, correctly, and inherited by every screen that uses the component (Accessible Component Patterns).
- Theming is a token-layer capability, not a component-layer one. If the semantic token layer exists, a re-theme is a set of value swaps; if it does not, it is a search-and-replace across the component source (Custom Properties).
What this makes the browser do
And which of it is avoidable.
- A shared component library is JavaScript that every consuming application ships. Without tree shaking and side-effect-free modules, importing one button can pull in the whole library (Tree Shaking).
- Token-driven theming through CSS custom properties costs style recalculation when a token changes, on the subtree that inherits it — which is exactly what makes a runtime theme switch feasible (Custom Properties).
- Deeply generic components pay at run time for their genericity: extra wrapper elements, extra prop normalisation, extra conditional branches per render (What a Component Costs to Render).
- Component-level CSS delivered per component can multiply requests or duplicate declarations across chunks; delivered as one stylesheet it is render-blocking for every page including the ones using three components (Render-Blocking Resources).
- Icon sets are the classic quiet cost: importing an icon module that registers three hundred icons so that a page can render four.
Four layers, one direction
The layering is the architecture. Each layer may depend on the one below it and nothing else, and the value of the whole system comes from that constraint holding. A component that hardcodes #1f6feb instead of consuming a semantic token has not just made a small mess; it has removed itself from every future theme, and it will be found during the rebrand, by a designer, in a screenshot.
The layer that teams most often skip is patterns. Components solve widgets; patterns solve tasks. "How do we ask for confirmation before something destructive", "what does an empty state contain", "how is a form laid out and where do its errors go" are decisions worth making once, and they are invisible in a library that stops at components.
- 1Primitive tokens
The raw palette and scales: every colour, every space step, every radius, every type size, each with a name and no opinion about use.
fails by Consumed directly by components, which couples the component to a specific colour rather than to a role, and makes re-theming impossible (Design Tokens).
- 2Semantic tokens
Roles rather than values: surface, text-on-surface, border-subtle, danger, focus-ring. This is the layer a theme swaps.
fails by Missing entirely, so there is nothing to re-point at a new palette and every theme is a fork.
- 3Components
Interactive primitives with real semantics, keyboard behaviour, focus handling and announcement, consuming only semantic tokens (Accessible Component Patterns).
fails by Built as styled
divelements, industrialising one accessibility defect across every product that adopts it. - 4Patterns
Recurring compositions: form layout with error placement, destructive confirmation, empty state, data table with sorting and pagination.
fails by Left out, so five teams each invent a different confirmation flow and the product is consistent at the button and incoherent at the task.
- 5Applications
Compose patterns, own page-level structure: headings, landmarks, routing, focus on navigation, and the words (Document Structure and Reading Order).
fails by Forking a component to change one detail, which permanently disconnects that screen from every future fix.
- 6Release
Versioned publish with a changelog, migration notes and codemods for mechanical breaking changes.
fails by Breaking changes shipped as patches, which teaches every consumer to pin and ends upgrades (Versioning: What a Version Even Promises in API Design).
Read the failure column as the design system's actual risk register. Each one is recoverable early and close to permanent once four products depend on it.
The API you did not mean to publish
The moment a consumer can observe something, it is part of your contract. Class names, DOM structure, the order of children, which element receives focus, and whether a wrapper element exists are all things consumers will build on, and all things you will want to change. The narrower the surface you expose deliberately, the more you can change without a major version.
The code below is the same component twice. The difference is not style; it is how much of the implementation is reachable from outside. The second version can be rewritten entirely — different DOM, different internal structure, different implementation of the focus ring — without breaking a consumer, because everything a consumer can rely on is named.
1// Leaks its internals: every consumer can depend on the DOM,2// so every internal change is a breaking change.3export function Button(props: { className?: string; onClick?: () => void; children: React.ReactNode }) {4 return (5 <div className={`ds-btn ${props.className ?? ''}`} onClick={props.onClick} role="button">6 <span className="ds-btn__inner">{props.children}</span>7 </div>8 )9}10// consumer, now permanent: .ds-btn__inner { padding: 0 }11 12// Names its contract, hides its structure.13export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {14 /** what the button is for, not what it looks like */15 variant?: 'primary' | 'secondary' | 'danger'16 /** the only supported way to render an icon alongside the label */17 icon?: React.ReactNode18}19 20export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(21 function Button({ variant = 'secondary', icon, children, ...rest }, ref) {22 // real element: type, keyboard activation, focus, disabled, form23 // participation and the accessibility role all come from the platform24 return (25 <button ref={ref} type="button" data-variant={variant} {...rest}>26 {icon}27 {children}28 </button>29 )30 },31)The second version exposes a variant role rather than a colour, forwards a ref because consumers legitimately need the node, spreads native attributes so aria-*, form and disabled work without the library enumerating them — and renders a real button, which is where the keyboard behaviour and the accessibility role come from at no cost.
Adoption is the only metric that matters
A design system nobody uses is a cost centre with a roadmap. Adoption is not a communications problem and it is rarely fixed by a mandate: teams adopt a shared component when using it is cheaper than writing their own, including the cost of every future upgrade. That means the escape hatch, the migration tooling and the responsiveness of the maintainers are load-bearing parts of the architecture, not soft extras.
The decision below is the one a consuming team actually faces, several times a quarter, when the shared component does not quite do the thing. The system's job is to make the first two options genuinely available, because the third and fourth are how systems die.
A team needs behaviour the design system does not provide. What happens next?
when The system anticipated variation here and exposed a named extension point — a slot, a documented style hook, an unstyled primitive underneath the styled one.
cost Every extension point is API surface the maintainers can no longer change freely, so the system must choose them deliberately rather than adding one per request.
when The need is general, at least two teams have it, and the maintainers can review and release on a timescale that does not block the product.
cost The requesting team pays review latency and takes on the burden of a general design rather than the one that solves their screen. Only viable if upstream turnaround is measured in days.
when The variation is genuinely small and orthogonal to everything already there.
cost This is the option that feels cheapest and compounds worst: prop count grows monotonically, the combinatorial surface becomes untestable, and the component eventually has no coherent model (Over-Componentization).
when Almost never — genuinely only when the product is diverging on purpose and permanently, and the divergence is documented as such.
cost A permanent disconnection from every future fix, including accessibility and security fixes. Forks are invisible in adoption metrics until an audit finds four buttons and one of them cannot be operated by keyboard.
How to build it
Most important first.
- Build the token layer first, and make it the only source of visual values. Components that consume semantic tokens are re-themeable by construction; components with literals are not, and no amount of later discipline fixes that (Design Tokens).
- Make each component correct by default: real semantics, full keyboard operation, managed focus, sensible announcement. This is the single highest-leverage place in a frontend organisation to make accessibility the default, because getting the button right once makes every screen's button right (Accessible Component Patterns).
- Design the public API deliberately and keep it small: named props with meaning, slots for content you cannot anticipate, events for behaviour the consumer must own. Everything you do not expose you can change (What a Component Owes Its Caller).
- Version with semantic versioning that consumers can trust, publish a changelog that names the migration for every breaking change, and provide codemods for the mechanical parts. Adoption is mostly a function of how cheap upgrading is.
- Deprecate on a schedule with an overlap window: the replacement lands, the old one warns, and only then is it removed. A removal with no warning period teaches teams to pin (Deprecation as a Process, Not a Label in API Design).
- Provide a documented escape hatch — a slot, a style hook, an unstyled primitive — so that a team with an unusual need extends the system instead of forking it. Every fork is a permanent divergence.
- Treat adoption as the primary metric. Component usage per product, number of local reimplementations, and version lag say more about the system's health than the size of its component list.
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- This is the highest-leverage accessibility work available to a frontend organisation. A correct
Button,Dialog,ComboboxandField— real semantics, keyboard behaviour, focus management, announcement — make the default outcome correct for every screen, including screens built by people who have never read the guidance (Accessible Component Patterns). - It also concentrates the risk. A regression in the shared focus trap is a regression in every modal in the company, which is why the design system needs stronger automated and manual accessibility testing than any product using it (Accessibility Testing).
- Some things components cannot own and must document: heading level, landmark structure, page-level focus order and the order fields appear in a form. These belong to patterns and to the application, and a system that does not say so leaves them to nobody (Document Structure and Reading Order).
- The token layer carries accessibility constraints too: contrast is a property of a colour pair, so semantic pairs — surface and the text on it — must be validated together and re-validated for every theme (Contrast, Colour and Motion).
- Motion tokens should honour the user's reduced-motion preference at the token layer, so that a component author cannot forget it (Contrast, Colour and Motion).
What can go wrong
- The library that only the design system team uses. It is complete, documented, beautifully built and absent from the product, because it was designed without the consuming teams in the room.
- Version fragmentation: three products on three majors, one shared screenshot showing three button styles, and an upgrade nobody can schedule.
- Prop explosion: every consumer need answered with a new boolean until the component has forty props, no coherent model and a combinatorial test surface (Over-Componentization).
- Internals-as-API: consumers reach into class names and DOM structure, so a refactor that changes nothing public breaks four products.
- Accessibility asserted rather than tested. The library claims conformance, no product tests keyboard operation, and a regression in the shared focus trap ships to every consumer at once (Accessibility Testing).
- The mitigation failing: an escape hatch so broad that consumers use it to override everything, which reproduces the inconsistency the system exists to prevent while adding a package to maintain.
- A token change and a component change released separately can land in a consumer in either order, so a component that assumes a token exists must degrade rather than render a broken value.
- During a rollout, two versions of the same component can be present in one page — through a transitive dependency or a micro-frontend boundary — and both will render (Micro Frontends).
- A shared library is a supply-chain concentration point: one compromised release reaches every product at once, and it renders on every screen (Typosquatting and Malicious Packages in Security).
- Components that accept HTML — a rich-text renderer, a slot documented as accepting markup, a tooltip that takes formatted content — are shared injection sinks. The library must sanitise or refuse, because every consumer will assume it did (Sanitization and Trusted HTML).
- Icon and font assets loaded from a third-party origin extend every consuming product's trust boundary and its Content-Security-Policy (Content Security Policy).
- Pinning versions is a security decision as much as a stability one: teams that cannot upgrade cheaply do not receive fixes, and version lag is where known vulnerabilities live (Dependency Security in Security).
- "A design system is a component library." The components are one of four layers. Without tokens it cannot be themed, without patterns it does not solve whole tasks, and without versioning and support it is not a product (Design Tokens).
- "Once it is published the work is done." Publishing is where the work starts: adoption, migration support, deprecation and answering consumers is the ongoing job, and an unmaintained system diverges faster than no system.
- "Consistency means every screen looks identical." It means a person does not have to relearn a control. A system that cannot express a legitimate product difference will be forked, and the fork is worse than the difference.
- "Teams will adopt it because it is mandated." They adopt it when it is cheaper than the alternative. Mandates without ergonomics produce wrappers around the shared component that reimplement half of it.
- "The design system owns accessibility." It owns component-level semantics and behaviour. Heading order, landmark structure, page focus flow, form order and error copy are the application's, and a system that implies otherwise creates a gap that nobody is watching (Semantics Before ARIA).
Measuring it, and what changes in the field
- Adoption: how many components from the system render on a given page, and how many local reimplementations exist alongside them. A static scan of the consuming repositories answers this better than a survey.
- Version lag per consumer, tracked over time. Rising lag means upgrading is too expensive, which is a design system problem rather than a consumer problem.
- Accessibility coverage of the library itself: automated checks per component, plus a recorded manual keyboard and screen-reader pass per interactive pattern (Accessibility Testing).
- Visual regression on the library and on a representative consumer, so that a token change is visible before it reaches four products (Visual Regression Testing).
- Bundle contribution of the system in each consuming application, which is the user-facing cost of the shared abstraction (Bundle Analysis).
- With one product and one team, the system is overhead: the coordination cost is paid and the consistency benefit is small, because one team is already consistent.
- With many products, the leverage grows superlinearly and so does the cost of a mistake, because every mistake is replicated everywhere.
- On a slow device, generic components with deep wrapper trees cost more per render than the bespoke component they replaced, and that is a real trade rather than a rounding error (What a Component Costs to Render).
- During a rebrand or a dark-mode launch, the token layer decides whether the work is days or quarters — this is the moment the architecture is tested (Design Tokens).
- Consistency costs flexibility. Every product team gives up local optimisation in exchange for not solving the same problem five times, and a system that never says no to a request becomes a component library with forty props per component.
- A stable public API costs the maintainers their ability to refactor freely. That is the price of being depended upon, and pretending otherwise produces breaking changes labelled as patches.
- Doing accessibility properly at the component layer is slower per component than not doing it, and it is the cheapest possible place to do it — the alternative is the same work repeated per screen, done by people with less context and no test coverage.
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERALThe four-layer structure, the public API obligation and the adoption problem are true of design systems in any technology — a Figma library, a web component set, a React package and a native iOS framework all have the same layering and the same failure modes, and they differ only in how the token layer is delivered.
- FRAMEWORK-SPECIFICA React or Vue component library couples every consumer to that framework and its major versions, while web components decouple the runtime at the cost of weaker typing, awkward slot composition and styling boundaries that differ across implementations; the choice determines who can adopt the system at all.
- PLATFORM-SPECIFICAccessibility behaviour written once still varies in how assistive technologies present it: the same correct combobox is announced differently by different screen reader and browser pairings, so component-level testing must cover the pairings your users have rather than one reference setup.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Software Design — a design system is the clearest real example of designing for consumers you cannot change: interface segregation, stable abstractions and the cost of a published API all show up here with names and version numbers attached.
- — Testing & Reliability Engineering — a shared library needs a stronger test suite than anything consuming it, because a regression is replicated into every product simultaneously and is discovered by users rather than by the team that caused it.