← Front-End Developer — all chapters  ·  Kelick Tech Hub

Chapter 41 — Design Systems and Reusable UI

Section 1 — What a design system is

A design system is a shared set of standards, components, and guidance that lets a team build interfaces consistently, without reinventing the same button, form field, or card for every page. It is more than a collection of components, although components are its most visible part. It also includes design tokens for colour, type, and spacing, principles for how the product should look and feel, documentation explaining when to use each component, and processes for proposing, reviewing, and updating those pieces. Together, those parts form a single source of truth: when a designer and a developer both refer to the design system, they mean the same button, the same spacing, and the same behaviour, rather than two similar but different versions.

The problem a design system solves is inconsistency at scale. On a small site built by one person, consistency is easy to maintain by memory. On a large product built by many teams over years, memory fails: one team builds a primary button with rounded corners and a shadow, another builds one with square corners and no shadow, and a third builds one with a different colour entirely.

A design system also speeds up work, once it exists. Building a new page from system components is faster than designing and coding each element from scratch, because the components are already tested, accessible, and styled. That speed is not free: building the system itself takes time, and maintaining it takes ongoing effort, so a design system only pays off when it is used repeatedly across many pages or products. For a single small site, a full design system may be overkill, and a simple style guide or a small component library may suffice. The decision to invest in a system should be based on how much reuse you realistically expect, not on fashion, because an unused system is a cost with no benefit.

It helps to think of a design system as a product in its own right, with users who are the designers and developers building the main product. Like any product, it needs to solve real problems for its users, it needs documentation and support, and it needs feedback and iteration. If components are hard to find, hard to use, or do not meet teams' needs, teams will work around the system, building their own versions, and the system will fail, however well-designed its components are in isolation. Successful systems are therefore built with their users, through interviews, trials, and contributions, not imposed from above as a set of rules that ignore real project needs.

Finally, a design system is never finished. Products evolve, brands change, accessibility standards improve, and new patterns emerge, so the system must evolve with them, through versioning, deprecation, and migration, which later sections cover. That evolution is a strength, not a flaw, because it means the system stays relevant, but it requires governance: someone must decide what changes, when, and how teams are supported through changes. A design system is thus as much about people and process as it is about code and pixels, and understanding that human side is essential to building one that lasts.

Check: Why does a design system pay off only when its components are reused repeatedly?

Section 2 — System, style guide, and component library

Three terms are often confused: design system, style guide, and component library, and distinguishing them helps you scope work correctly. A style guide is a document that describes visual standards, such as colours, typography, spacing, and logo usage, usually as examples and rules, but without working code. A component library is a collection of working, reusable interface components, such as buttons, inputs, and cards, implemented in code, with props or options for variation. A design system is the larger whole that includes both, plus tokens, principles, documentation, usage guidance, and governance. In short, a style guide tells you how things should look, a component library gives you working pieces, and a design system tells you why, when, and how to use them, and how they evolve.

That distinction matters when you are asked to "build a design system" for a project. If the project needs only consistent colours and fonts, a style guide may be enough, and building a full system would waste time. If the project needs reusable code, a component library may be enough, and adding principles and governance may be premature. If the project is large, long-lived, and built by many teams, a full system, with tokens, components, documentation, and governance, is justified, because the coordination problems it solves are real.

A pattern library is a related term, sometimes used for a collection of larger patterns, such as page layouts, navigation patterns, or form flows, built from components. Patterns sit above components in scale: a component is a button, a pattern is a login form built from inputs, a button, and error handling. A mature design system often includes patterns as well as components, because patterns capture decisions that recur at a larger scale, such as how to handle errors across forms, or how to structure a settings page. Patterns are valuable, but they should be built after components are stable, because a pattern built from unstable components will need rebuilding when those components change.

Another related concept is a design token system, covered in the next section, which is the layer of named values, such as colours and spacings, that underlies both styles and components. Tokens are part of a design system, but they can also exist alone, as a shared set of values used by multiple products without a full component library. That modularity is useful: you can adopt tokens first, then components, then patterns, growing the system incrementally as needs emerge, rather than building everything at once. Incremental growth is usually safer than a big-bang system launch, because each layer can be tested and adopted before the next is built.

When evaluating an existing system, ask which of these layers it actually provides. Does it have working code, or only pictures? Does it have usage guidance, or only examples? Does it have a process for updates, or is it a snapshot that will drift? Those questions reveal whether you are looking at a style guide, a library, or a true system, and they reveal what work remains to make it usable for your project. Naming the layer correctly is not pedantry; it sets expectations for what the system can do, what it costs to maintain, and what teams can rely on it for.

Check: What does a design system include that a component library alone does not?

Section 3 — Design tokens

Design tokens are named values for design decisions, such as colours, font sizes, spacing, radii, and shadows, stored in one place and used everywhere, instead of hard-coding values in many files. For example, instead of writing a specific hex colour in dozens of stylesheets, you define a token named color-primary, with that colour as its value, and you use the token everywhere the primary colour is needed. If the brand colour changes, you update the token once, and every component that uses it updates automatically. That single point of change is the core benefit, and it applies to every kind of value, not just colour: spacing tokens, type tokens, and shadow tokens all work the same way, turning scattered magic numbers into named, shared decisions.

Tokens are usually organised in layers. Primitive tokens are raw values, such as a palette of greys or a scale of spacings, with names that describe the value, such as grey-500 or space-4. Semantic tokens are meaningful names built on primitives, such as color-text-primary or space-section, which describe purpose rather than value, and which can change per theme, such as light and dark mode, without changing the components that use them. Component tokens are specific to a component, such as button-padding or card-radius, built on semantic tokens, which allow a component to be tuned without affecting others.

Naming tokens well is essential, because a token name is a contract: every developer who uses it must understand what it means. Names should describe purpose, not appearance, where possible, because appearance may change: color-primary is better than color-blue, because the primary colour could become green in a rebrand, while color-blue would then be misleading. Similarly, space-large is better than space-32px, because the large spacing value could change from thirty-two to twenty-four pixels, while the name large would still make sense. Avoid names that are too specific to one component, unless the token is genuinely component-specific, because a token named header-padding cannot be reused for a card without confusion, even if its value is appropriate.

Tokens can be implemented in several ways, depending on the stack. In CSS, custom properties, such as --color-primary, are a natural implementation, because they are native, themeable, and can be changed at runtime. In a design tool, tokens may be defined as styles or variables that are exported to code. In a multi-platform system, tokens may be defined in a neutral format, such as JSON, and transformed into platform-specific outputs, such as CSS variables for web, and constants for mobile, by a build tool. Whichever implementation you choose, the principle is the same: one source of truth for values, referenced by name, with a clear layering from primitive to semantic to component.

Finally, tokens must be documented and governed, like any shared code. A token catalogue, showing each token's name, value, and usage, helps teams find the right token instead of inventing a new value. A process for adding, renaming, or removing tokens, with deprecation warnings for old names, prevents the token set from growing chaotically or breaking existing components silently. Tokens are a small idea with a large impact: they turn design decisions into maintainable code, and they are the foundation on which the rest of a design system, from components to themes, is built.

Check: Why is a semantic token name like color-text-primary better than a value name like color-grey-800?

Section 4 — Colour tokens

Colour tokens turn a product's palette into a usable system, rather than a collection of hex values scattered through code. Start with a primitive palette: a set of hues, each with a scale of lightness steps, such as a blue scale from very pale to very dark, a grey scale from white to near-black, and scales for success, warning, and error colours. Those primitives are raw material, not yet decisions: they describe what colours are available, not what they mean. Semantic tokens then assign meaning: color-background, color-surface, color-text-primary, color-text-secondary, color-border, color-primary, color-success, and so on, each pointing to a primitive. Components use semantic tokens, so they describe their intent, and themes can remap semantic tokens to different primitives, as later sections explain for dark mode.

Contrast is the critical constraint for colour tokens. Text colours must contrast sufficiently with their backgrounds to be legible, according to accessibility standards, and that contrast must hold for every combination that actually occurs: primary text on background, secondary text on surface, white text on primary button, and so on. A palette that looks harmonious in a swatch may fail contrast for some combinations, so test each semantic pairing, not just the palette as a whole.

A common mistake is to create too many colour tokens, one per component or per shade, which defeats the purpose of a system by recreating scattered values under token names. A good token set is small enough to learn and large enough to cover real needs: a handful of backgrounds, surfaces, texts, borders, and action colours, plus a few status colours, is usually enough for most products. If a designer needs a new colour for a specific case, ask whether an existing token already serves that purpose, or whether the case reveals a genuinely new semantic need, such as a new status or a new surface type. Adding tokens sparingly keeps the system coherent and makes it easier for new team members to learn.

Another mistake is to name colour tokens by their usage in one place, such as header-background, when the same colour is used in many places, because that name will confuse future users who need it elsewhere. Semantic names should describe the role, such as surface or background, not the location, unless the token is truly location-specific. Component tokens, such as button-primary-background, are the exception: they are location-specific by design, and they should be built on semantic tokens, so a button's background can be tuned without changing every surface that uses the same semantic colour. That layering, primitive to semantic to component, keeps colour decisions both consistent and adjustable.

Finally, document colour tokens visually, with swatches showing each token's colour, its name, its primitive value, and its intended usage, including contrast pairings. That catalogue is the reference designers and developers will use when choosing colours, and it prevents the common workflow of picking a colour from a design file by eye and hard-coding it, which bypasses the system. A colour system that is documented, contrast-tested, and used consistently gives a product a coherent, accessible visual identity, which is far more valuable than a larger, untested palette that looks impressive in isolation but fails in use.

Check: Why should colour token contrast be tested per pairing rather than per palette?

Section 5 — Typography tokens

Typography tokens define a product's text styles as named, reusable values, rather than as ad-hoc font sizes and weights scattered through stylesheets. A typographic scale typically includes a set of sizes, such as a base size for body text, larger sizes for headings, and smaller sizes for captions, each with a corresponding line height, and sometimes a weight and letter spacing. Tokens name those styles by purpose, such as text-body, text-heading-1, text-heading-2, and text-caption, so a component uses a named style rather than a specific pixel size. That naming ensures that headings look like headings everywhere, body text is consistent, and a change to the scale, such as increasing body size for readability, updates every component that uses it.

Choosing a type scale requires balancing readability, hierarchy, and restraint. A scale with too many sizes, such as ten distinct heading sizes, creates a hierarchy that is hard to learn and hard to maintain, because designers must choose among many similar options. A scale with too few sizes may not distinguish headings clearly enough, making pages feel flat.

Line height, or leading, is as important as size, and it should be part of typography tokens, not left to defaults. Body text needs generous line height, typically around one and a half times the font size, for comfortable reading, while large headings need tighter line height, because their lines are shorter and tighter spacing looks more cohesive. Letter spacing also matters: large headings may benefit from slightly tighter tracking, while small uppercase labels may need looser tracking to remain legible. Including those values in tokens, rather than setting them per component, ensures that text styles are complete and consistent, not just sizes that leave other properties to chance.

Font choice is another token decision. A product typically uses one or two typefaces: one for most text, and optionally a second for headings or for a distinctive brand voice. Using more than two typefaces rarely improves a product and often makes it feel disjointed, while also increasing loading time if web fonts are used. Tokenise the font family as well as sizes, so components refer to font-body and font-heading tokens, rather than hard-coding typeface names, which makes it easy to change typefaces globally or per theme. Also consider system font stacks, which use fonts already on the user's device, as an alternative to web fonts, because they load instantly and feel native, which may suit some products better than a custom brand typeface.

Finally, typography tokens must be tested for accessibility and for responsiveness. Text sizes should scale sensibly on small screens, where a large heading that works on desktop may dominate a phone screen, and they should respect user preferences, such as increased text size in the browser, rather than fixing sizes in a way that prevents zooming. Use relative units, such as rem, for token values, so text scales with the user's base size, and test with browser zoom and with larger system text sizes, confirming that layouts do not break and that hierarchy remains clear. Typography tokens are not just about making text look good; they are about making it readable for everyone, at every size, which is the core purpose of type in an interface.

Check: Why should line height be part of a typography token rather than left to a default?

Section 6 — Spacing, radius, and shadow tokens

Spacing tokens define the distances between elements as a small, consistent scale, rather than as arbitrary pixel values chosen per component. A typical spacing scale might be based on a base unit, such as four or eight pixels, with tokens for multiples: space-1, space-2, space-4, space-8, and so on, or semantic names such as space-tight, space-standard, and space-loose. Using that scale means gaps, padding, and margins across a product are related, not random, which gives layouts a rhythm that feels intentional. It also makes spacing decisions faster, because designers and developers choose from a scale rather than debating whether a gap should be thirteen or fourteen pixels, a distinction no user can perceive but which creates inconsistency if left unchecked.

Radius tokens define corner rounding, from sharp corners to fully rounded pills, as named values such as radius-small, radius-medium, radius-large, and radius-full. Corner rounding affects a product's character: sharp corners feel precise and technical, large radii feel friendly and soft, and inconsistent radii feel careless. Tokenising radius ensures that cards, buttons, inputs, and images share a consistent rounding language, with larger elements using larger radii and smaller elements using smaller ones, so the product feels coherent. A common mistake is to use a different radius for every component, which makes the product look assembled from unrelated parts, even if each component looks fine alone.

Shadow tokens define elevation, the sense that some elements float above others, through box shadows of varying blur, spread, and opacity. A shadow scale might include levels such as shadow-1 for subtle cards, shadow-2 for raised panels, and shadow-3 for modals and dropdowns, each with a defined shadow value. Elevation should be used sparingly and consistently: higher elevation should mean higher priority or greater temporariness, such as a modal above a page, not merely decoration. Too many shadow levels, or shadows used randomly, create a confusing depth hierarchy, where a low-priority card appears to float above a high-priority modal. Keep shadow levels few, and document what each level means.

These three token types work together to define a product's spatial language. Spacing controls how elements relate horizontally and vertically, radius controls how their corners feel, and shadow controls how they layer in depth. Together, they determine whether a product feels dense or airy, sharp or soft, flat or layered, which are character decisions, not just technical ones. That character should be deliberate: a dense, sharp, flat product suits a technical tool, while an airy, rounded, layered product suits a consumer app, and mixing those characters without intention creates a product that feels unsure of itself.

Finally, spacing, radius, and shadow tokens must be tested at real sizes and in real layouts, because a spacing scale that works on desktop may feel cramped on a phone, and a shadow that is subtle on a light background may be invisible on a dark one. Use relative units where appropriate, test with different content lengths, and check that shadows do not cause performance problems when animated, as earlier chapters warn. Tokens for space, radius, and shadow are small values, but they shape every layout in a product, and getting them consistent is one of the highest-leverage investments in a design system.

Check: Why should spacing values come from a scale rather than being chosen per component?

Section 7 — Naming tokens well

Naming is one of the hardest parts of a design system, because a name must be clear to designers and developers, stable across changes, and specific enough to be useful without being so specific that it cannot be reused. Good token names describe purpose, not value, as earlier sections explain: color-primary is better than color-blue, and space-large is better than space-32px, because purposes are stable while values change. That principle applies to all tokens, and to component names as well: a button named primary-button is clearer than one named blue-button, because its role is primary, even if its colour changes. Purpose-based naming is the single most important naming habit in a design system.

Names should also be consistent in structure, so they can be guessed and searched. A common structure is category, then purpose, then variant, such as color-text-primary, color-text-secondary, space-section-large, or button-primary-background. That order groups related tokens together when sorted alphabetically, which makes a token catalogue easier to browse, and it makes names predictable: once you know color-text-primary exists, you can guess color-text-secondary. Inconsistent structures, such as primary-text-color in one place and text-color-secondary in another, make tokens hard to find and hard to remember, which leads teams to invent new tokens rather than reuse existing ones, defeating the system's purpose.

Avoid names that are too long, too vague, or too clever. A name like color-interactive-element-hover-state-background is precise but cumbersome, and developers will abbreviate it informally, creating unofficial variants. A name like color-thing is vague and gives no clue to its purpose. A name based on a joke, a person's name, or a brand campaign will confuse future team members who do not share that context. Aim for names that are short, descriptive, and boring, because boring names are clear, stable, and easy to use, while clever names age poorly and create confusion. If a name needs a comment to explain it, the name itself is probably not clear enough.

Names should also avoid encoding the implementation, such as platform, file, or technology, unless the token is genuinely platform-specific. A token named web-color-primary cannot be reused for mobile without confusion, even if its value is appropriate, so prefer platform-neutral names for shared tokens, and add platform-specific tokens only where values genuinely differ. Similarly, avoid names that encode a current design trend, such as flat or material, because trends change and the name will become misleading. Names should describe what a token is for, in terms that will remain true as the product evolves, not how it is currently implemented or styled.

Finally, treat renaming as a governed change, not a casual edit, because renaming a token breaks every component that uses it, unless aliases or deprecation are provided. When a name must change, provide an alias from the old name to the new one for a transition period, update documentation, and communicate the change to teams, so they can migrate without breaking their work. That discipline is part of token governance, and it reflects a general truth: names are interfaces, and changing an interface requires care, communication, and a migration path, just as changing a component's API does.

Check: Why is a purpose-based token name more stable than a value-based one?

Section 8 — Theming with tokens

Theming is the ability to change a product's appearance, such as switching between light and dark mode, or between brands, by remapping tokens, without changing component code. The mechanism is simple in principle: components use semantic tokens, such as color-background and color-text-primary, and a theme defines what values those tokens have. In light mode, color-background might be white and color-text-primary might be near-black. In dark mode, color-background might be near-black and color-text-primary might be white. Components do not change; only the token values change, and the whole product updates consistently, because every component refers to tokens, not to hard-coded colours.

Implementing themes in CSS is natural with custom properties. Define semantic tokens on a root element for the default theme, and redefine them under a theme selector, such as a data attribute for dark mode, or a media query for the user's system preference. For example, a dark theme might be applied with a selector that sets --color-background to a dark value and --color-text-primary to a light value, while component styles that use those variables remain unchanged.

Dark mode is the most common theme, and it deserves care beyond simply inverting colours. Inverting a light palette often produces harsh, high-contrast dark colours that are uncomfortable to read, because pure white text on pure black background creates a halo effect and eye strain. A good dark theme uses dark greys, not pure black, for backgrounds, off-white, not pure white, for text, and slightly desaturated accent colours, because saturated colours that work on white can glare on dark. Also, shadows, which add depth in light mode, are less visible in dark mode, so elevation may need to be conveyed with lighter surfaces or borders instead, as earlier sections note. Test dark mode with real content, not just with swatches, because its legibility depends on actual text and backgrounds, not on palette previews.

Beyond dark mode, themes can serve other purposes: brand themes for white-label products, where the same application is sold to different companies with different colours and logos; accessibility themes, such as high-contrast mode, where colours are chosen for maximum legibility; and seasonal or marketing themes, where a product's appearance changes temporarily for an event. Each of those themes follows the same token mechanism, but each requires its own contrast and legibility testing, because a theme that looks good in a preview may fail for some combinations in real use. Themes multiply the testing surface, so keep the number of themes small and well-tested, rather than offering many themes that are each poorly tested.

Finally, themes should respect user preference and should be easy to switch and persist. A common pattern is to follow the system preference by default, using the prefers-color-scheme media query, and to allow the user to override it with a setting that is saved, so their choice persists across visits. That setting should be applied early, before the page paints, to avoid a flash of the wrong theme, which can be jarring. Theming through tokens is a powerful feature, but its value depends on careful colour choices, thorough testing, and respect for user preference, not merely on the mechanism of swapping variables.

Check: Why should a dark theme use dark greys rather than pure black for backgrounds?

Section 9 — Buttons in a system

Buttons are the most fundamental component in a design system, and getting them right sets the standard for all other components. A button component typically has variants, such as primary, secondary, and tertiary or ghost, which signal importance: primary for the main action on a page, secondary for supporting actions, and tertiary for low-emphasis actions such as cancel or a link-like action. It also has sizes, such as small, medium, and large, for different contexts, and states, such as default, hover, focus, active, disabled, and loading, each with a distinct appearance. Those variants, sizes, and states together define the button's API: the options a developer can choose when using it, and the combinations that are allowed or discouraged.

Designing button variants requires restraint. Too many variants, such as five different primary styles, create confusion about which to use, and teams will choose inconsistently. A small set, with clear guidance on when each is used, is better: one primary per page or section, secondary for other actions, and tertiary for the least important. Guidance should include examples of correct and incorrect use, such as not using two primary buttons side by side for competing actions, because that undermines the primary's signal. That guidance is part of the system, not an optional extra, because components without guidance will be misused, however well they are built.

Button states must be designed together, as in the previous chapter's discussion of feedback. Hover, focus, active, disabled, and loading states should each be distinct, with sufficient contrast, and with transitions that are fast and consistent, using motion tokens. Focus states must be highly visible and must not rely on colour alone, because keyboard users depend on them. Disabled states must look clearly inactive and must not respond to hover feedback that implies clickability. Loading states must show that work is in progress and must prevent duplicate submissions, as earlier sections explain. Those states are not edge cases; they are part of the button's normal behaviour, and a button component that lacks them is incomplete.

Button content also deserves guidance. Labels should be concise, action-oriented, and specific, such as "Save changes" rather than "OK" or "Submit", because specific labels help users predict what will happen. Buttons should not contain long sentences, and they should not be used for navigation where a link is more appropriate, because buttons imply an action, while links imply navigation, and confusing those signals confuses users. Icon buttons, which show only an icon, must have an accessible label, because an icon alone may be ambiguous, and they must be large enough to tap comfortably, with a minimum target size that meets accessibility guidelines.

Finally, button components must be tested in all variants, sizes, and states, in both themes, and with different label lengths, because a button that works with a short English label may break with a long label in another language, or with a large text size setting. Test buttons with keyboard navigation, with screen readers, and on touch devices, confirming that each state is reachable, announced, and tappable. A button is a small component, but it is used everywhere, so its quality disproportionately affects the product's overall quality, which is why it deserves the most careful design and testing in a system.

Check: Why should a page normally have only one primary button per section?

Section 10 — Button variants and states in code

Building a button component in code requires an API that is clear, flexible, and hard to misuse. In HTML and CSS, that API might be classes, such as btn, btn-primary, btn-secondary, btn-small, and btn-large, combined to express variant and size, with disabled and loading handled by attributes or additional classes. In a framework, the API might be props, such as variant, size, disabled, and loading, with TypeScript types or documentation limiting them to allowed values. Whichever approach you use, the API should make correct use easy and incorrect use hard: allowed variants should be enumerated, not free-form strings, so a typo cannot create a broken button, and combinations that do not make sense, such as a primary ghost button, should be prevented or warned about, rather than silently rendering a confusing result.

A common implementation pattern is to define base button styles once, for layout, padding, font, border radius, and transitions, and then to define variant styles that only change colours and borders, and size styles that only change padding and font size. That separation keeps the code maintainable, because a change to base padding affects all buttons, while a change to primary colour affects only primary buttons, and it prevents duplication, where each variant repeats the same base styles with small differences that drift over time.

States in code should be handled with native mechanisms where possible. Disabled should use the disabled attribute on a button element, which natively prevents interaction and is announced to assistive technology, rather than a class that merely looks disabled but remains clickable. Loading should also disable the button, and should replace or supplement its label with a spinner and a loading text, as earlier sections explain, with the loading state managed by the component's logic, not by the caller manually swapping classes. Hover, focus, and active states should be handled by CSS pseudo-classes, not by JavaScript, because CSS handles them reliably for mouse, keyboard, and touch, without extra code, and because they are presentation states, not application states.

Accessibility in button code includes using the correct element: a button element for actions, an anchor element for navigation, and not a div with a click handler, which lacks keyboard support and semantics. It also includes ensuring that the button's accessible name matches its visible label, so screen reader users hear the same text that sighted users see, and that icon-only buttons have an aria-label that describes their action.

Finally, document the button component with examples of each variant, size, and state, with code snippets, and with guidance on when to use each variant, as in the previous section. Documentation should be part of the component, not a separate document that drifts, and it should be generated or maintained alongside the code where possible, so examples stay correct as the component evolves. A button component with a clear API, token-based styles, native state handling, correct semantics, and good documentation is a model for how all components in a system should be built, and it sets a standard that other components can follow.

Check: Why should a disabled button use the disabled attribute rather than only a disabled-looking class?

Section 11 — Inputs, labels, and forms

Input components are where users enter data, and their design affects whether forms are completed correctly or abandoned in frustration. The core of an input component is not just the field itself, but its label, its hint, its error message, and its states, all designed together. A label should be visible, associated with its field programmatically, and placed above the field in most cases, because placeholder text alone is not a label: it disappears when the user types, it is hard to read for low-vision users, and it is not reliably announced by screen readers. A hint, which explains what is expected, such as a format example, should be separate from the label and should not disappear on input, so users can refer to it while typing.

Input states mirror button states, but with additional nuances. A default state shows the field ready for input, with a clear border and background.

Input types matter as well. A text input, an email input, a password input, a number input, a date input, and a textarea each have different behaviours, keyboards on mobile, and validation rules, and a design system should provide components or guidance for each, rather than forcing all inputs into a single text field. On mobile, the correct input type triggers the appropriate keyboard, such as an email keyboard with an at sign, or a numeric keyboard for numbers, which makes entry faster and less error-prone. Autocomplete attributes should also be set correctly, such as for name, email, and address fields, so browsers can autofill them, which reduces typing and errors. Those attributes are part of the component's API, not an afterthought, because they affect real user experience.

Validation and error handling are part of input design, not just application logic. Errors should be shown near the field they relate to, not only in a summary at the top of the form, although a summary can help for long forms by listing all errors with links to their fields. Errors should appear at a sensible time: showing errors before the user has interacted with a field can feel punitive, while showing them only on submit can delay feedback. A common pattern is to validate on blur, after the user leaves a field, and on submit, with live validation for some fields, such as password strength, where immediate feedback helps the user succeed. Whatever timing you choose, it should be consistent across the form, and it should be documented, so users learn when to expect feedback.

Finally, form components should be tested as a whole, not just individually, because forms are where components interact: labels, hints, errors, and buttons must work together, and a form that is accessible field by field can still be confusing as a whole if its order, grouping, or error summary is poor. Test forms with keyboard navigation, with screen readers, with different input methods, and with real user tasks, such as completing a registration or a checkout, because those tasks reveal problems that component tests miss. Inputs are the most error-prone part of most interfaces, so investing in their design, states, and validation pays off in completion rates and in user trust.

Check: Why is placeholder text not an acceptable substitute for a visible label?

Section 12 — Selects, checkboxes, and radios

Selects, checkboxes, and radios are choice components, each suited to a different kind of choice, and confusing them is a common design error. A checkbox is for a single independent choice that can be on or off, such as agreeing to terms, or for multiple choices where several can be selected, such as choosing interests. A radio button is for a single choice from a set where only one can be selected, such as choosing a shipping method, and radios in a set must be grouped so that selecting one deselects the others. A select dropdown is for a single choice from a long list, such as a country, where showing all options at once would take too much space, or for a choice where the list is long and searchable. Choosing the right component for the choice type is the first design decision, before styling or implementation.

Native elements for these components have strong accessibility and behaviour built in, and customising them requires care not to break that behaviour. A native checkbox or radio, styled with CSS, retains keyboard support, screen reader announcements, and focus handling, while a fully custom component built from divs must reimplement all of that, which is error-prone.

Select dropdowns are more complex, because native selects vary across browsers and operating systems, and custom selects are often built to achieve a consistent appearance or features such as search. However, custom selects are notoriously hard to make accessible, because they must handle keyboard navigation, focus management, announcements, and touch behaviour, all of which native selects handle for free. For most cases, a native select, styled as much as possible with CSS, is the safer choice, even if its appearance varies slightly across platforms, because that variation is familiar to users and its behaviour is reliable. A custom select should only be built when its additional features, such as search or multi-select with tags, are genuinely needed, and it should be built with accessibility as a primary requirement, not as a retrofit.

Grouping and labelling for choice components are essential. Checkboxes and radios should be grouped in a fieldset with a legend that describes the group, such as "Shipping method", so screen readers announce the group context for each option. Each option should have its own label, associated with its input, and the label should be clickable, so tapping the text toggles the option, which increases the tap target and improves usability on touch.

States for choice components include checked, unchecked, indeterminate for checkboxes that represent a group with mixed selections, disabled, and focus, each with a distinct visual. The indeterminate state, shown as a dash in the checkbox, is useful for "select all" controls, where some but not all items are selected, and it should be set programmatically, because it is not a state users can set directly. Test choice components with keyboard navigation, confirming that arrow keys move between radios, space toggles checkboxes, and focus is visible, and with screen readers, confirming that group, option, and state are announced. Choice components are small, but they carry decisions, and getting their semantics right is essential for users to make those decisions confidently.

Check: When should you use radios rather than checkboxes for a set of options?

Section 13 — Cards and content containers

Cards are containers that group related content, such as an image, a title, a description, and actions, into a single, bounded unit, and they are one of the most common patterns in modern interfaces. Cards work well for lists of similar items, such as products, articles, or profiles, because they give each item a consistent structure, making scanning easier: the user learns where the title, image, and price or action are in one card, and can apply that knowledge to every card in the list. Cards also work well for dashboards, where each card can hold a metric, a chart, or a summary, with a consistent size and spacing that makes the dashboard feel organised, even when its content varies.

Designing a card component requires deciding its structure and its variations. A typical card has a media area, such as an image, a body area with a heading and text, and a footer area with actions or metadata, but not every card needs all areas, and variations should be limited to meaningful ones, such as a card with media, a card without media, and a compact card for dense lists.

Cards must handle content variation gracefully. A card with a long title should not break its layout, a card without an image should not leave an awkward gap, and a card with many actions should not overflow its bounds. That resilience requires testing with real content, including long titles, missing images, and varying text lengths, and designing the card to flex or truncate sensibly, rather than assuming ideal content. Images in cards should have consistent aspect ratios, so cards in a grid align, and they should have alt text that describes the image's purpose, not just its appearance, because cards are often scanned by screen readers as a group, and meaningless alt text adds noise.

Interactivity for cards needs care. A card that is entirely clickable, such as a product card that links to a detail page, should be implemented as a single link or button, with its internal actions, such as a favourite button, as separate interactive elements that do not nest inside the link, because nested interactive elements are invalid and confusing for assistive technology. A common pattern is to make the card's title a link, and to use a stretched link technique, where the link's clickable area is extended to cover the card visually, while other actions remain separate and reachable.

Finally, cards should not be used where a simpler pattern would suffice. A list of text items does not need cards, because cards add visual weight and spacing that can make a simple list feel heavy and slow to scan. A single piece of content does not need a card, because a card implies it is one of a set. Cards are for grouped, comparable items, and using them indiscriminately dilutes their meaning. When cards are used appropriately, with consistent structure, resilient layout, and careful interactivity, they make complex content scannable and approachable, which is why they are a staple of design systems, but they should be chosen deliberately, not by default.

Check: Why must a card's internal buttons not be nested inside its main link?

Section 14 — Modals, dialogs, and overlays

Modals, dialogs, and overlays are layers that appear above the main page to focus attention on a specific task, such as confirming an action, editing a setting, or viewing details, and their design must balance focus with disruption. A modal interrupts the user's flow, so it should only be used when interruption is justified, such as for a destructive confirmation, a required input, or a task that needs the user's full attention. For less critical information, such as a help tip or a preview, a less disruptive pattern, such as an inline expansion, a popover, or a side panel, may be better, because it does not block the underlying page or trap focus. Choosing the right overlay type is the first decision, before designing its appearance.

A modal component typically includes a backdrop, which dims the page behind it, a panel with a heading, content, and actions, and a close control, usually an X button and often a backdrop click or Escape key to dismiss. The panel should be centred or positioned consistently, with a maximum width and height, and with scrolling inside the panel if its content exceeds the viewport, rather than scrolling the whole page.

Focus management, as discussed in the previous chapter, is critical for modals. When a modal opens, focus should move into it, be trapped within it, and return to its trigger when it closes, so keyboard users are not lost behind the overlay. That behaviour must be implemented in the modal component, not left to each caller, because it is easy to forget and hard to test per use. The modal should also be announced to screen readers as a dialog, with its heading as its accessible name, and its content should be reachable in a sensible order. Those accessibility behaviours are part of the component's contract, and they should be tested for every modal, not just for the first one built.

Modal sizes and variations should be limited. A system might offer small, medium, and large modals, for different content amounts, but it should not offer a custom size for every modal, because that creates inconsistency and makes modals harder to test. Similarly, modal types, such as confirmation, form, and information, should be variations of the same component, with consistent backdrop, panel, and close behaviour, differing only in content and actions, rather than separate components with different behaviours. That consistency helps users learn what a modal means in the product, so they can predict how to dismiss it and where focus will go, without relearning each time.

Finally, modals should be used sparingly, because each modal is an interruption, and frequent interruptions frustrate users and train them to dismiss modals without reading them, which defeats their purpose for genuinely important confirmations. Audit modal usage periodically, and replace modals that are used for routine information with less disruptive patterns, reserving modals for tasks that truly require focused attention. A design system that provides a good modal component but also guides teams on when not to use it is more valuable than one that merely provides the component, because restraint in modal use protects the modal's effectiveness when it is truly needed.

Check: When is a modal the wrong choice, and what less disruptive pattern might suit instead?

Section 15 — Navigation components

Navigation components help users move through a product, find content, and understand where they are, and they include headers, sidebars, tabs, breadcrumbs, and pagination, each suited to a different navigation need. A header, or top navigation, typically holds a logo, primary navigation links, search, and user actions, and it should be consistent across pages, so users always know where to find navigation. A sidebar, or side navigation, suits products with many sections, such as dashboards or admin tools, because it can list many sections vertically without crowding the top, and it can collapse on small screens to a drawer, as earlier chapters discuss. Choosing between header and sidebar, or using both, depends on the product's information architecture and on how many sections it has, not on fashion.

Tabs are for switching between views of the same context, such as different sections of a profile, or different filters for a list, without leaving the page. Tabs should be implemented as a tablist with tab buttons and tab panels, with keyboard support for arrow keys to move between tabs, and with the selected tab indicated clearly, not only by colour, but by a border, background, or underline, and by an aria-selected attribute for assistive technology.

Breadcrumbs show the user's path through a hierarchy, such as Home, then Products, then a specific product, and they help users understand where they are and navigate back up the hierarchy quickly. Breadcrumbs should reflect the actual hierarchy, not the user's history, because history-based breadcrumbs, which show pages visited, are unpredictable and less useful for orientation. They should be placed consistently, usually near the top of the content, and they should be marked up as a list with an aria-label, so screen readers announce them as navigation. Breadcrumbs are most valuable in deep hierarchies, where users may arrive from search or a link and need context; in shallow products with few levels, they may be unnecessary and can be omitted without harm.

Pagination and related controls help users navigate long lists, and they include numbered pages, previous and next buttons, and sometimes a page size selector. Pagination should show the current page clearly, provide previous and next controls, and, for long lists, show a sensible range of page numbers with ellipses for skipped pages, rather than listing every page, which becomes unwieldy. Infinite scroll is an alternative for some lists, such as feeds, where users browse without a specific target, but it has drawbacks: it makes footers unreachable, it makes returning to a specific item hard, and it can be inaccessible if not implemented with care.

Finally, navigation components must be tested as a system, not just individually, because users experience navigation as a whole: header, sidebar, tabs, and breadcrumbs must agree on the current location, must not duplicate the same links in confusing ways, and must work together on small screens, where some navigation may collapse or move. Test navigation with keyboard, with screen readers, and on phones, confirming that the current location is always clear, that navigation is reachable, and that it does not trap focus or hide content. Navigation is the product's map, and a map that is inconsistent or incomplete will lose users, however good the individual pages are.

Check: Why are tabs unsuitable for a sequential checkout flow?

Section 16 — Tabs and accordions as components

Tabs and accordions are both ways to organise content into sections, but they serve different needs, and a design system should provide both with clear guidance on when to use each. Tabs show one section at a time, with all section titles visible as a row, so users can switch quickly between sections and see what sections exist at a glance. Accordions show section titles as a stack, with each section expanding to reveal its content, and multiple sections may be open at once, depending on the design, so users can compare sections or read them in sequence. Tabs suit a small number of sections, typically fewer than six, with short titles, where switching is frequent. Accordions suit a larger number of sections, or sections with long content, where stacking is more space-efficient than a row of tabs that would wrap or overflow.

Implementing tabs as a component requires the tab pattern from accessibility standards: a tablist container, tab buttons with roles, and tab panels that are shown or hidden based on the selected tab. Keyboard support should include arrow keys to move between tabs, Home and End to jump to first and last, and Tab to move into the panel content, which is the standard tab interaction that keyboard users expect.

Accordions as a component require a similar pattern: a heading with a button that toggles its section, with aria-expanded indicating whether the section is open, and with the section content hidden when collapsed, using visibility or hidden attributes in addition to height animation, so it is not focusable when closed. The accordion's animation, as discussed in the previous chapter, should reflect the actual content height, not an arbitrary max-height, and it should be short, so content becomes available promptly. Accordions may allow multiple sections open, or only one at a time, and that behaviour should be a documented option of the component, not a per-use invention, because users will learn the accordion's behaviour from one instance and expect it in others.

Choosing between tabs and accordions for a given content set depends on several factors: number of sections, length of titles, length of content, frequency of switching, and screen size. On small screens, tabs with many sections may overflow or wrap awkwardly, while accordions stack naturally, so a responsive design might use tabs on desktop and accordions on mobile for the same content, with the component system supporting that switch.

Finally, both tabs and accordions should be tested with real content and with assistive technology, confirming that section titles are clear, that switching is fast and predictable, and that content is available in a sensible order. Avoid using tabs or accordions to hide essential content that users need to see together, such as a form's required fields split across tabs, because users may miss a required field in a hidden tab and fail to submit, without understanding why. Tabs and accordions organise content; they should not hide it in a way that creates errors, and their guidance should warn against that misuse, with examples of better patterns, such as a single scrolling form with clear sections, for cases where all content must be seen.

Check: When is an accordion a better choice than tabs for organising sections?

Section 17 — Tables as components

Tables are components for structured, comparable data, and they deserve design system treatment, because a well-designed table component makes data scannable, sortable, and accessible, while a poorly designed one makes data hard to use. A table component typically includes a header row with column titles, body rows with data, and often features such as sorting, filtering, pagination, and row selection, each of which must be designed and implemented consistently. The table's visual design should prioritise readability: clear column alignment, with numbers right-aligned and text left-aligned, sufficient row spacing, and subtle row separators or zebra striping, used sparingly, so rows are distinguishable without adding visual noise.

Sorting is a key table feature, and its design should be clear and predictable. Sortable columns should indicate that they are sortable, with an icon or a hint, and should show the current sort direction, ascending or descending, with an icon and, for screen readers, with text or an aria-sort attribute on the header. Sorting should be stable, meaning rows with equal values retain their original order, and it should be announced to screen readers, so users know the table has reordered.

Responsive tables are a challenge, because tables with many columns do not fit on small screens. Options include horizontal scrolling, where the table scrolls within its container while its header remains visible, which preserves the table structure but requires users to scroll sideways; stacking, where each row becomes a card-like block with labels, which works for simple tables but loses the ability to compare columns; and hiding less important columns on small screens, which preserves comparability for key columns but hides data that some users may need. The best approach depends on the data and the task, and a design system should provide guidance and, where possible, components for each approach, rather than leaving teams to invent a responsive table per project, which often results in broken or inaccessible tables on phones.

Accessibility for tables includes proper markup: a caption or aria-label describing the table's purpose, header cells with scope attributes, and, for complex tables, headers and ids to associate data cells with their headers. Those markup details are essential for screen reader users, who navigate tables by headers and need to know which column and row a cell belongs to. A table component should generate that markup automatically, so developers using it do not need to remember scope attributes, and it should be tested with a screen reader, confirming that headers are announced and that sorting and pagination are operable. A table that looks good but lacks proper headers is not a finished table component.

Finally, tables should be used only for tabular data, not for layout, because using tables for layout creates accessibility and maintenance problems, and modern CSS layout, such as grid, is better suited to layout. That distinction, which early web developers often blurred, should be part of the system's guidance, with examples of when a table is appropriate, such as a list of orders with comparable columns, and when it is not, such as a two-column page layout. A table component that is well-designed, responsive, accessible, and used only for data will serve many products, while a table component that is misused for layout will create problems that are hard to undo.

Check: Why should sortable table headers use aria-sort as well as a visual icon?

Section 18 — Badges, tags, and chips

Badges, tags, and chips are small, compact components that convey status, category, or a filter, and they are often confused, but they have distinct uses. A badge typically shows a count or a status, such as a number of unread messages, or a label such as "New" or "Beta", attached to another element, such as an icon or a heading. A tag typically labels content with a category, such as a topic or a skill, and it is usually not interactive, or it links to a filtered view of that category. A chip typically represents a selected filter, an input, or a choice, and it is often interactive, with a remove control, such as an X, to deselect it. Distinguishing those uses helps teams choose the right component, rather than using a badge for a filter or a chip for a status, which confuses users about what will happen when they interact with it.

Designing these small components requires attention to size, colour, and text. They should be small enough to sit inline with text or beside an element, but large enough to read and, if interactive, to tap, with a minimum tap target that meets accessibility guidelines, even if the visual chip is small, by increasing its hit area.

Interactive chips, such as filter chips that can be removed, need careful accessibility. A chip with a remove button should be implemented as a group, with the chip's label and a separate button to remove it, rather than as a single button that both selects and removes, because those are two different actions. The remove button should have an accessible label, such as "Remove filter: JavaScript", rather than just "X", so screen reader users know what will be removed. Keyboard users should be able to focus the remove button and activate it, and focus should move sensibly after removal, such as to the next chip or to the filter control, rather than disappearing. Those details are easy to miss in a small component, but they determine whether chips are usable by all users.

Counts in badges also deserve care. A badge showing a count, such as unread messages, should update accurately, should not show zero when there are no unread items, unless zero is meaningful, and should cap large counts, such as showing "99+" for counts above ninety-nine, rather than a long number that breaks the badge's layout. That capping should be documented, so teams implement it consistently, rather than each team choosing a different cap.

Finally, these components should be used sparingly. A page covered in badges, tags, and chips feels noisy and makes it hard to distinguish what matters, because every item is shouting for attention. Reserve badges for genuinely new or urgent status, tags for meaningful categories that help filtering or discovery, and chips for active filters or inputs that the user has chosen. That restraint, combined with consistent sizing, colour, and accessibility, makes small components effective, because they stand out precisely because they are not everywhere. A design system that provides these components with clear guidance on when to use each, and when not to use them, helps teams avoid the clutter that comes from overusing small, eye-catching elements.

Check: What is the difference between a badge, a tag, and a chip?

Section 19 — Alerts, toasts, and banners

Alerts, toasts, and banners are components for communicating messages to users, each with a different level of urgency and persistence, and choosing the wrong one can cause users to miss important information or to be interrupted unnecessarily. An alert, or inline message, appears within the page content, near the related content, and persists until dismissed or resolved, such as a form error or a warning about unsaved changes. A toast, or snackbar, appears temporarily, usually at the edge of the screen, to confirm an action, such as "Saved", and then disappears automatically, without requiring dismissal. A banner appears at the top of the page or section, often for site-wide information, such as maintenance or a new feature, and it persists until dismissed, but it does not block the page like a modal.

Designing alerts requires clear visual distinction by type: success, info, warning, and error alerts should each have a distinct colour, icon, and, where possible, a distinct shape or border, so their type is clear without colour alone. Alerts should include a concise title and a clear message, with an action where appropriate, such as "Retry" for an error, and a dismiss control where the alert is dismissible, with an accessible label for that control.

Toasts require careful timing and placement. They should appear for a duration long enough to read, typically several seconds, depending on message length, and they should not disappear so quickly that users miss them, nor linger so long that they block content. Toasts should be dismissible, with a close control, and they should pause their timer on hover or focus, so users who are reading them do not lose them mid-read. Toasts should not be used for critical errors or for information users must act on, because their temporary nature means users may miss them, and a missed critical message is a failure. Critical messages belong in alerts or modals, where they persist until acknowledged, not in toasts that vanish.

Banners require similar care, but at a larger scale. A site-wide banner should be dismissible, with its dismissal persisted, so users are not shown the same banner on every page load after dismissing it, which is frustrating and trains users to ignore banners. Banners should not push content down in a way that causes layout shift when they appear or disappear, because that shift can cause users to click the wrong element, so their space should be reserved or their appearance should be handled without shifting layout abruptly.

Finally, message components should be tested with real messages, including long messages, messages with actions, and messages in different languages, because a message component that works with a short English message may break with a long message or with a different script. Test their announcements with screen readers, their dismissal with keyboard, and their timing with users who read slowly, because those tests reveal problems that visual design alone misses. Message components are how a product communicates with its users, so their clarity, timing, and accessibility directly affect trust, and they deserve the same care as any other component in a system.

Check: Why should critical errors not be shown only as temporary toasts?

Section 20 — Avatars, tooltips, and popovers

Avatars, tooltips, and popovers are small components that provide identity, explanation, and additional information, each with its own accessibility and interaction considerations. An avatar typically shows a user's photo or initials, in a circular or rounded shape, and it serves as a visual identifier in lists, comments, and headers. Avatars should have alt text or an accessible label that identifies the person, such as their name, rather than generic alt text such as "avatar", because a generic label gives screen reader users no information. If a photo is unavailable, initials should be used, with a background colour that contrasts with the text, and with a consistent size and shape across the product, so avatars are recognisable as a pattern, not as random circles.

Tooltips provide additional information on hover or focus, such as explaining an icon's meaning or giving a definition, and they are often misused for essential information, which is a problem, because tooltips are not available to all users: touch users cannot hover, keyboard users may not trigger hover tooltips, and screen reader users may not receive tooltip content unless it is implemented accessibly. A tooltip's content should therefore be supplementary, not essential, and essential information should also be available in visible text or in the element's accessible name.

Popovers are similar to tooltips, but larger and more interactive, often containing actions, forms, or rich content, such as a user card or a filter panel, that appears near a trigger. Popovers should be dismissible by clicking outside, by pressing Escape, and by activating a close control, and focus should move into the popover when it opens if it contains interactive content, and return to the trigger when it closes, similar to modals but less disruptive, because popovers do not block the whole page.

Implementation for tooltips and popovers should use native mechanisms where possible, such as the title attribute for simple tooltips, although native tooltips have styling and accessibility limits, or a well-tested component library's tooltip, rather than a hand-rolled implementation that misses focus, dismissal, or positioning edge cases. If a custom tooltip or popover is built, it should be tested with mouse, keyboard, touch, and screen readers, confirming that its content is available, that it can be dismissed, and that it does not trap focus or hide essential information.

Finally, these small components should be used with restraint and with clear guidance. Avatars should not be used as the sole identifier for a person, because a photo alone does not identify someone to a screen reader user or to a user who does not recognise the photo; names should accompany avatars where identification matters. Tooltips should not be used to hide instructions that should be visible, because hiding instructions behind hover creates a discoverability problem. Popovers should not be used for content that deserves its own page or modal, because cramming complex content into a small popover makes it hard to use. Small components are most effective when they supplement, not replace, clear, visible information.

Check: Why must tooltip content be supplementary rather than essential information?

Section 21 — Pagination and breadcrumbs in code

Pagination and breadcrumbs, introduced as navigation patterns, have specific implementation details that a design system should standardise, because inconsistent pagination or breadcrumb code across pages confuses users and creates accessibility bugs. Pagination in code typically renders a list of page links, with previous and next controls, and with the current page indicated as such, using aria-current="page" on the current link, so screen readers announce it as the current page. Page links should be real links, not buttons that merely change state without updating the URL, where pagination corresponds to different URLs, because real links allow bookmarking, sharing, and back-button navigation, which users expect for paginated content. If pagination is client-side, with no URL change, it should still update the page's state in a way that is bookmarkable or at least navigable, or its limitations should be documented.

Pagination logic includes deciding which page numbers to show. For a small number of pages, show all numbers.

Breadcrumbs in code should be a list, typically an ordered list, with an aria-label such as "Breadcrumb", and with each item except the current one as a link, and the current item as plain text with aria-current="page", so screen readers announce the hierarchy and the current location correctly. Separators between breadcrumb items, such as slashes or chevrons, should be decorative, hidden from screen readers, rather than read aloud as "slash" between each item, which adds noise. Breadcrumb items should reflect the hierarchy, not the user's history, as earlier sections explain, and they should be generated from the site's structure, not hand-written per page, where possible, so they stay correct as the structure changes. That generation, from routing or from a site map, is part of the breadcrumb component's value, because hand-written breadcrumbs drift out of date.

Both pagination and breadcrumbs must handle long labels and small screens. Pagination on a phone may need to show fewer page numbers, or only previous, current, and next, with a compact layout, rather than a full row that overflows. Breadcrumbs on a phone may need to collapse to show only the parent and current items, with the full hierarchy available on demand, rather than wrapping over multiple lines and pushing content down. Those responsive behaviours should be part of the components, with tests at small sizes, rather than left to each page to handle, because inconsistent responsive behaviour across pages makes navigation feel unreliable.

Finally, pagination and breadcrumbs should be tested with keyboard and screen readers, confirming that page links are focusable in order, that the current page is announced, that disabled controls are not actionable, and that breadcrumb separators are not announced. Test them with real data, including many pages and deep hierarchies, because edge cases, such as a single page, where pagination should be hidden, or a top-level page, where breadcrumbs should be hidden or show only the current item, are where bugs most often appear. Navigation components that are correct in the common case but broken at the edges will frustrate users precisely when they need navigation most, such as when they are deep in a large list or hierarchy.

Check: Why should pagination pages be real links rather than buttons when they correspond to URLs?

Section 22 — Layout components

Layout components are the structural pieces of a design system, such as containers, grids, stacks, and clusters, that arrange content without adding visual styling of their own. A container typically centres content and limits its maximum width, so text lines do not become too long on large screens, which harms readability. A grid arranges items in rows and columns, with consistent gaps, for card lists, dashboards, and galleries. A stack arranges items vertically with consistent spacing, for forms, articles, and lists, where vertical rhythm matters. A cluster arranges items horizontally, wrapping as needed, for tags, buttons, and navigation, where items should flow together without fixed columns. Those layout primitives, borrowed from modern CSS layout, give teams a shared vocabulary for structure, so they do not reinvent spacing and alignment per page.

The value of layout components is that they separate structure from content styling. A card component should not need to know whether it sits in a grid or a stack; it should simply be a card, and the layout component should decide how cards are arranged.

Implementing layout components in CSS can use modern features such as flexbox and grid, with tokens for gaps and for maximum widths. A stack component, for example, can be a simple class that sets display to flex, direction to column, and gap to a token value, with an optional exception for the first child, so spacing is consistent without needing margins on each child. A grid component can set display to grid, with a template that adapts to container size, such as auto-fill with a minimum column width, so the grid is responsive without media queries per page. Those implementations are short, but they should be standardised, because a stack implemented with margins in one place and with gap in another will behave differently, especially with collapsing margins and with hidden items, which gap handles more predictably than margins.

Layout components must also handle responsiveness and accessibility. A grid that works on desktop should adapt to phone widths, by reducing columns or stacking items, without requiring per-page media queries, which is why grid templates that use auto-fill or container queries are valuable. A layout component should not add visual styling, such as backgrounds or borders, because that would make it less reusable: a stack used for a form and a stack used for an article should look different only because their content differs, not because the stack itself adds styling.

Finally, layout components should be documented with examples, showing how containers, grids, stacks, and clusters combine to build common page structures, such as a dashboard, a form page, and an article page. Those examples help teams compose layouts from primitives, rather than building custom layouts per page, which is the goal of a layout system. When layout components are used consistently, pages feel coherent, spacing feels intentional, and maintenance becomes easier, because a change to a layout primitive, such as increasing grid gaps, updates every page that uses it, without requiring edits to each page's styles.

Check: Why should a layout component not add visual styling like backgrounds?

Section 23 — Icon systems

Icons are small, symbolic images that represent actions, objects, or concepts, such as a search magnifier, a trash can, or a settings gear, and an icon system is the shared set of icons, with consistent style, size, and usage, that a product uses. A good icon system starts with a defined style: stroke width, corner style, level of detail, and size grid, such as a twenty-four-pixel grid, that all icons follow, so they look like they belong together, rather than as a collection of icons from different sources with different weights and details. That consistency is visible even at small sizes, where an icon with a different stroke width will look bolder or thinner than its neighbours, breaking the visual harmony of a toolbar or navigation.

Icon implementation in front-end work is usually via SVG, because SVG icons scale cleanly, can be styled with CSS, and can be inlined or loaded as a sprite, with each icon as a symbol that can be referenced by id. That sprite approach is efficient, because the sprite is loaded once and reused, and it allows icons to be coloured with currentColor, so they inherit the text colour of their context, which is useful for icons in buttons, links, and navigation, where colour should match text.

Accessibility for icons is critical, because icons alone can be ambiguous or invisible to assistive technology. Decorative icons, which merely accompany text, should be hidden from screen readers, with aria-hidden, so they do not add noise, such as announcing "magnifier" before "Search". Meaningful icons, which are the sole content of a button, such as a trash icon for delete, must have an accessible label, either via aria-label on the button, or via visually hidden text, so screen reader users know what the button does. That distinction, decorative versus meaningful, should be part of the icon component's API, with a prop or attribute to set it, rather than left to each developer to remember, because forgetting it creates either noise or silence, both of which harm usability.

Icon usage guidance should cover when to use an icon, when not to, and how to pair icons with text. Icons should supplement text, not replace it, for important actions, because an icon's meaning is often culturally dependent or ambiguous: a gear may mean settings to some users, but not to all, and a floppy disk for save is familiar to older users but meaningless to younger ones who have never seen one.

Finally, icon systems must be governed, because icons accumulate over time, with new icons added for new features, and old icons becoming outdated or duplicated. A catalogue of approved icons, with names, usage, and examples, helps teams find an existing icon rather than adding a new, similar one, which creates duplication. A process for proposing, reviewing, and adding icons, with checks for style consistency and for accessibility, keeps the system coherent. An icon system that is catalogued, tokenised, accessible, and governed gives a product a consistent, efficient visual language, while one that grows unchecked becomes a clutter of mismatched symbols that confuse more than they clarify.

Check: When should an icon be hidden from screen readers, and when must it have a label?

Section 24 — Component APIs: props, slots, and composition

A component's API is the set of options it offers to its users, and designing that API well is as important as designing the component's appearance, because a confusing API will lead to misuse, even if the component looks good. In framework components, the API is typically props, which are named inputs, such as variant, size, disabled, and label, that control the component's behaviour and appearance. In web components or in HTML and CSS systems, the API might be attributes, classes, or slots, which are placeholders for content that the caller provides, such as a card's header, body, or footer. Whichever mechanism is used, the API should be clear, minimal, and consistent across components, so learning one component helps with learning others.

Props should be named consistently and should have clear types and defaults. A size prop should be named size in every component, not size in one, dimension in another, and scale in a third, because inconsistent naming forces developers to relearn each component. Prop values should be limited to allowed options, such as small, medium, and large, rather than free-form numbers or strings, because limited options prevent invalid combinations and make the component's behaviour predictable.

Slots and composition are an alternative to many props, where a component provides structure and the caller provides content. A card component, for example, might have slots for media, body, and footer, rather than props for title, text, and image, which allows the caller to compose arbitrary content within the card's structure, rather than being limited to the props the card designer anticipated. Composition is more flexible than props, but it is also less constrained, because callers can put inappropriate content in a slot, such as a form inside a card header, which breaks the card's design. A good component API balances props and slots: props for simple, constrained variations, such as variant and size, and slots for content that varies widely, such as a card's body, with guidance on what belongs in each slot.

Composition also applies at a larger scale: components should be designed to work together, such as a form component that composes input components, or a modal component that composes button components, rather than each component reinventing its parts. That composability requires components to be small, focused, and predictable, with clear boundaries: a button should not contain a form, a card should not contain a modal, and a layout component should not contain content styling. When components are composable, teams can build complex interfaces from simple pieces, without needing a new component for every combination, which is the core promise of a component system.

Finally, component APIs should be documented with examples, with types, and with guidance on composition, including do and do-not examples that show correct and incorrect uses. Documentation should be generated from code where possible, so it stays accurate as the API evolves, and it should include accessibility notes, such as which props affect semantics or keyboard behaviour, because API choices can have accessibility consequences that callers may not anticipate. A component API that is consistent, minimal, documented, and composable is a joy to use, and it encourages adoption, while an API that is inconsistent, sprawling, or undocumented discourages use and leads teams to build their own components instead.

Check: When is a slot better than a prop for a component's content?

Section 25 — Variants, composition, and flexibility

Variants are the predefined variations of a component, such as a button's primary, secondary, and tertiary styles, or a card's with-media and without-media layouts, and they are the primary way a design system offers flexibility without chaos. A variant should represent a meaningful difference in purpose, not merely a different colour or size for its own sake, because each variant adds to the system's complexity and to the learning burden for teams. If a variant does not correspond to a distinct use case, such as primary for main actions versus secondary for supporting actions, it should not exist, or it should be merged into another variant, because unnecessary variants create choice paralysis, where developers spend time choosing among similar options rather than building features.

Composition, as discussed in the previous section, is the other source of flexibility, where components are combined to build larger interfaces, rather than each larger interface being a new component. A good system encourages composition by providing small, focused components that work well together, and by documenting common compositions, such as a form composed of inputs, a modal composed of a header, body, and footer, or a dashboard composed of cards in a grid.

Flexibility must be balanced with constraint, because a system that is too flexible becomes no system at all, while a system that is too rigid forces teams to work around it. A useful principle is to make common cases easy, uncommon cases possible, and incorrect cases hard. Common cases, such as a primary button, should be a single prop or class. Uncommon cases, such as a button with a custom icon, should be possible through composition or an additional prop, but should require deliberate effort, so they are not used accidentally. Incorrect cases, such as a primary button with a disabled appearance but still clickable, should be prevented by the component's implementation, not merely discouraged in documentation, because documentation alone does not prevent bugs.

Escape hatches are a related concept: a documented way to go beyond the system's constraints when genuinely needed, such as a custom class or style override, with guidance on when it is acceptable and when it is not. Escape hatches are necessary, because no system can anticipate every need, and without them, teams will hack around the system in undocumented ways that are worse than a documented override.

Finally, variants and composition should be tested together, because a component that works in isolation may break when composed, such as a card that looks good alone but breaks in a grid with varying content lengths, or a button variant that works in light mode but fails in dark mode when combined with a specific background. Test components in their common compositions, not just alone, and document any composition constraints, such as a component that should not be nested inside another instance of itself, or a layout that requires a minimum width. A system that provides variants for purpose, composition for complexity, constraint for correctness, and escape hatches for genuine exceptions gives teams both speed and quality, which is the goal of reusable UI.

Check: What does "make common cases easy, uncommon cases possible, and incorrect cases hard" mean for a component?

Section 26 — Component states

Component states are the conditions a component can be in, such as default, hover, focus, active, disabled, loading, error, empty, and selected, and designing states systematically is essential, because users encounter components in all states, not just in their default, ideal state. A button, for example, is not just its default appearance; it is also its hover, focus, active, disabled, and loading appearances, each of which must be designed, implemented, and tested. A form field is not just its empty state; it is also its filled, focused, error, and disabled states, each with distinct visuals and behaviours. A list is not just its populated state; it is also its empty, loading, and error states, each with a designed message and action. Those states are part of the component, not edge cases to be handled per use.

A useful way to design states is to list them for each component, and to design each one deliberately, with a visual, a behaviour, and an accessibility consideration. For a button, hover might darken the background, focus might add a ring, active might scale slightly, disabled might reduce opacity and prevent interaction, and loading might show a spinner and disable the button.

States also interact: a component can be in multiple states at once, such as disabled and loading, or focused and error, and those combinations must be handled, with a clear priority. For example, a disabled button should not show hover feedback, even if the pointer is over it, because hover implies clickability, which disabled contradicts. A focused error field should show both focus and error indicators, not one or the other, because the user needs to know both that they are in the field and that it has an error. Defining state priorities, such as disabled overrides hover, and focus and error combine, prevents inconsistent behaviour where the same combination looks different in different components, because each component handled it ad hoc.

State design also includes transitions between states, which should be smooth but fast, using motion tokens, as earlier chapters discuss, and which should not delay the state change itself. A button's transition from default to hover should be quick, so feedback feels immediate, while a list's transition from loading to populated should not cause layout shift, because that shift can disorient users. State transitions should also respect reduced motion, by showing the new state instantly when that setting is active, rather than animating to it, because state clarity matters more than animation, and a user who prefers reduced motion should still receive clear state feedback, just without motion.

Finally, states must be documented and tested as part of the component, with examples of each state in the component's documentation, and with tests that verify each state's appearance, behaviour, and accessibility. A component catalogue that shows only default states gives a false impression of the component, because it hides the states users will actually encounter in real use, and it encourages teams to assume states are handled when they are not. Showing all states, including empty, error, and loading, in documentation and in tests, ensures that states are designed, not discovered, and that the component is truly reusable, because it handles the full range of conditions it will face in production.

Check: Why must state combinations, such as focused and error, be designed explicitly?

Section 27 — Accessibility in reusable components

Accessibility in reusable components is disproportionately important, because a component used in many places will propagate its accessibility, good or bad, to every place it is used. A button component with a clear focus style, correct semantics, and an accessible name will make every button in the product accessible, with no extra effort per use. A button component with a missing focus style, a div instead of a button element, or an inaccessible name will make every button in the product inaccessible, with a bug that must be fixed in the component, not per use, and that may have already reached users in many places. That leverage is why accessibility must be built into components from the start, not added later per page, because later addition requires fixing every use, while built-in accessibility fixes all uses at once.

Component accessibility includes several dimensions: semantics, keyboard support, focus management, contrast, and announcements. Semantics means using the correct element or role, such as button for actions, link for navigation, and dialog for modals, so assistive technology understands the component's purpose.

Testing component accessibility requires both automated and manual methods. Automated tools can check for missing alt text, insufficient contrast, missing labels, and incorrect roles, and they should be part of the component's test suite, running on every change, so regressions are caught early. Manual testing, with a keyboard and with a screen reader, catches problems automated tools miss, such as illogical tab order, confusing announcements, or focus that disappears after an action. Both methods are needed, because automated tools check rules, while manual testing checks experience, and a component can pass rules while still being confusing to use, if its order, announcements, or focus behaviour are poorly designed.

Accessibility also includes handling user preferences and settings, such as reduced motion, increased text size, high contrast, and dark mode, which earlier sections discuss. A component should work in all those contexts, not just in the default context, because users who need those settings are part of the audience, and a component that breaks under increased text size or in high contrast mode is not a finished component, however good it looks in the default. Test components with browser zoom, with system text size increased, with high contrast mode, and with reduced motion, confirming that they remain usable, legible, and operable in each context, because those tests reveal layout, contrast, and motion problems that default testing hides.

Finally, accessibility documentation should be part of component documentation, stating the component's keyboard interactions, its roles and attributes, its focus behaviour, and any accessibility considerations for its use, such as providing labels for icon-only buttons, or providing text alternatives for charts within cards. That documentation helps teams use components accessibly, because it tells them what the component already handles and what remains their responsibility, such as providing a meaningful label, rather than leaving them to guess. A component system that documents accessibility alongside appearance and API is a system that treats accessibility as a core quality, not as an optional extra, and that treatment is what makes its products genuinely usable by everyone.

Check: Why is an accessibility bug in a shared component worse than the same bug in a one-off page?

Section 28 — Documenting components

Documentation is what turns a collection of components into a usable design system, because components without documentation will be misused, ignored, or reinvented, however good they are. Good component documentation includes several parts: a description of what the component is for, examples of it in use, its API, including props, slots, and events, its variants and states, its accessibility notes, and its usage guidance, including do and do-not examples. Each part serves a different need: description helps teams decide whether the component fits their need, examples show how to use it, API details enable correct implementation, variants and states show its range, accessibility notes ensure it is used accessibly, and guidance prevents misuse, such as using a primary button for a secondary action, or using a modal for routine information.

Examples in documentation should be live and interactive where possible, so teams can see the component, try its variants, and copy working code, rather than reading static pictures that may not match the current implementation. Live examples also serve as tests, because if an example breaks, it reveals a component bug, whereas a static picture would hide it.

Usage guidance, with do and do-not examples, is often the most valuable part of documentation, because it captures decisions that are not obvious from the component's API alone. For example, guidance for a button might show a do example with one primary button per section, and a do-not example with two primary buttons competing, explaining why the latter confuses users. Guidance for a modal might show a do example for a destructive confirmation, and a do-not example for routine information, explaining why a less disruptive pattern suits the latter. Those examples, with explanations, help teams make good decisions, not just correct implementations, which is the difference between a component library and a design system: the system teaches judgement, not just syntax.

Documentation must be maintained alongside code, or it will drift and become misleading, which is worse than no documentation, because teams will follow outdated guidance and build incorrect interfaces. A common approach is to generate documentation from code comments, prop types, or component metadata, so API details stay accurate automatically, and to review documentation as part of code review, so guidance is updated when a component changes. Documentation should also be versioned with the component, so teams using an older version can find documentation for that version, rather than being confused by documentation for a newer version they do not yet use. That versioning is part of system governance, covered later.

Finally, documentation should be discoverable and searchable, with a clear catalogue, search, and navigation, because documentation that cannot be found will not be used, however good it is. It should also be written in plain language, with short sentences and clear headings, because its audience includes designers, developers, product managers, and new team members, not just experts in the component's implementation. Good documentation is a product in itself, with users, feedback, and iteration, just as the components are, and investing in it pays off in adoption, correct use, and reduced support, because teams can answer their own questions from documentation, rather than asking the system team repeatedly.

Check: Why is outdated documentation worse than no documentation for a component?

Section 29 — Usage guidance: do and do-not

Usage guidance, often presented as do and do-not examples, is the part of a design system that teaches judgement, not just implementation, and it is what distinguishes a system from a library. A library tells you how to use a component; guidance tells you when to use it, when not to, and what to use instead. For example, guidance for a primary button might say do use one primary button per section for the main action, and do not use primary buttons for secondary actions, because that dilutes the primary's signal. Guidance for a modal might say do use a modal for a destructive confirmation, and do not use a modal for routine information, because that trains users to dismiss modals without reading. Those rules, with visual examples, help teams make decisions that are consistent across the product, even when the system team is not involved.

Good guidance is specific, not vague. A rule such as "use buttons appropriately" gives no actionable help, while a rule such as "use a primary button only for the main action on a page, and use secondary buttons for other actions, with at most one primary per section" gives a clear, testable rule that teams can follow and reviewers can check.

Do and do-not examples should be visual, showing the correct and incorrect use side by side, with the same content, so the difference is clear. A do example for a form might show labels above fields, with clear error messages, while a do-not example shows placeholder-only labels, with errors only as red borders, explaining why the former is accessible and the latter is not. Those visual pairs are more effective than text alone, because they let teams see the difference, not just read about it, and they are easier to remember. They should also be realistic, using real content and real layouts, not abstract diagrams, because realistic examples transfer better to real work.

Guidance should also cover edge cases and common mistakes, because those are where teams most often need help. For example, guidance for a table might cover what to do when a table has only one row, or when a column has very long values, or when a table is empty, because those edge cases are where a generic table design breaks. Guidance for a card might cover what to do when a card has no image, or when its title is very long, because those variations occur in real data.

Finally, guidance should be treated as a living document, updated based on feedback, on observed misuse, and on new patterns that emerge in real products. If teams frequently misuse a component in the same way, that misuse may reveal a gap in guidance, or a gap in the component itself, such as a missing variant that teams need, and the system team should respond by improving guidance or the component, rather than merely blaming teams for misuse. That feedback loop, from usage to guidance to system improvement, is part of governance, and it ensures that guidance remains relevant, rather than becoming a set of rules that no longer match how teams actually work.

Check: Why should usage guidance explain why a rule exists, not just state the rule?

Section 30 — Versioning components

Versioning is how a design system manages change over time, so teams can adopt updates predictably, without their interfaces breaking unexpectedly. The most common approach is semantic versioning, where version numbers have three parts: major, minor, and patch. A patch version, such as 1.2.3 to 1.2.4, indicates a bug fix that should not break existing use. A minor version, such as 1.2.0 to 1.3.0, indicates a new feature that is backward compatible, such as a new variant or a new prop with a sensible default. A major version, such as 1.0.0 to 2.0.0, indicates a breaking change, such as removing a prop, renaming a component, or changing its default appearance in a way that affects existing uses, which requires teams to update their code. That structure gives teams a language for change: they can adopt patches automatically, minors with testing, and majors with planned migration.

Breaking changes deserve special care, because they affect every team using the component, and they can cause widespread breakage if not communicated and supported. A breaking change should be justified by a significant benefit, such as fixing an accessibility bug, improving performance, or aligning with a new brand, not by a minor aesthetic preference, because the cost of migration across many teams is high.

Deprecation is the bridge between old and new APIs. When a prop, variant, or component is deprecated, it should still work, but it should warn developers, in documentation and, where possible, in code, that it will be removed in a future major version, and it should suggest the replacement. That warning period gives teams time to migrate at their own pace, rather than being forced to migrate immediately on a major release, which can disrupt their own schedules. Deprecation warnings should be clear and actionable, stating what is deprecated, what to use instead, and when it will be removed, because vague warnings, such as "this will be removed soon", give teams no plan and are often ignored until removal actually breaks their code.

Versioning also applies to tokens and to documentation, not just to components. A token rename is a breaking change, because it breaks every component and page that uses the old name, unless an alias is provided, as earlier sections discuss for naming. A documentation change that reflects a new version should be versioned with that version, so teams on an older version can find documentation for their version, rather than being confused by documentation for a version they do not use.

Finally, versioning requires communication and coordination, because a design system is used by many teams with different schedules and priorities. A system team should announce releases, with release notes, migration guides, and support channels, and should listen to feedback about migration difficulty, because a release that is technically correct but practically hard to migrate will be adopted slowly, creating a fragmented product where some teams use the new version and others use the old, which undermines consistency. Versioning is therefore not just a numbering scheme; it is a social contract between the system team and its users, promising predictable, communicated, and supported change, which is essential for trust in a shared system.

Check: What is the difference between a minor version change and a major version change for a component?

Section 31 — Breaking changes and migration

Breaking changes are inevitable in a long-lived design system, because products evolve, accessibility standards improve, and early decisions prove wrong, but how they are handled determines whether teams trust the system or resist it. A breaking change might be removing a deprecated prop, changing a component's default behaviour, renaming a token, or altering a component's markup in a way that affects styling or accessibility. Each of those changes breaks some existing use, and the system team must weigh the benefit against the migration cost across all teams, not just the benefit in isolation. If the benefit is small and the cost is large, the change should be postponed, redesigned to be backward compatible, or implemented as a new variant alongside the old one, rather than as a breaking replacement.

Migration support is what makes breaking changes acceptable. A migration guide should show, for each breaking change, the old code, the new code, and the reason for the change, with examples that teams can copy and adapt, rather than abstract descriptions that leave teams to guess the new usage.

Timing and communication for breaking changes also matter. Breaking changes should be grouped into major releases, rather than released piecemeal, so teams can plan one migration effort, rather than many small ones that disrupt their schedules repeatedly. Major releases should be announced well in advance, with a timeline, with support from the system team, such as office hours or migration workshops, and with a clear end-of-life date for the old version, after which it will no longer receive updates, including security or accessibility fixes. That timeline gives teams a deadline, but also a reasonable period to migrate, rather than an abrupt cutoff that forces rushed, error-prone migration.

A related strategy is to provide both old and new versions in parallel for a transition period, such as offering a new button component alongside the old one, with a different name or version, so teams can migrate page by page, rather than all at once. That parallel approach is more work for the system team, because it must maintain two versions, but it is often easier for product teams, because it allows incremental migration, which is less risky than a big-bang migration that changes every button in a product at once. Incremental migration also allows teams to learn the new component gradually, and to report problems early, before they affect the whole product, which improves the new component's quality.

Finally, after a migration, the system team should review what went well and what went badly, by gathering feedback from teams, measuring migration time and problems, and updating its process for future breaking changes. That retrospective is part of governance, and it ensures that breaking changes become easier over time, as the system team learns how to communicate, support, and automate migrations more effectively. Breaking changes will never be painless, but with clear justification, good migration guides, automated tools, reasonable timelines, and parallel support where needed, they can be manageable, and teams will trust the system to evolve without breaking their work unexpectedly, which is essential for a system's long-term adoption.

Check: Why should breaking changes be grouped into major releases rather than released piecemeal?

Section 32 — Building components in plain HTML and CSS

Building components in plain HTML and CSS, without a framework, is a valuable starting point for a design system, because it teaches the fundamentals of structure, styling, and accessibility, and it produces components that work in any stack, without framework dependencies. A plain HTML and CSS component is typically a set of classes, such as card, card-media, card-body, and card-footer, combined in a predictable markup structure, with styles defined in a stylesheet that uses tokens for colours, spacing, and type. That approach is simple, transparent, and easy to debug, because the component's markup and styles are visible in the page, without framework abstraction, and it can be used in static sites, in server-rendered pages, and as the basis for framework components later.

The main challenge in plain HTML and CSS components is consistency of markup. Without a framework component to enforce structure, developers must copy markup correctly per use, and they may vary it, omitting a class, nesting elements differently, or adding extra wrappers, which can break styling or accessibility.

Styling plain components should use tokens and should avoid overly specific selectors, because specific selectors, such as a card style that only applies when a card is inside a particular container, make the component less reusable and harder to override. A common CSS architecture for plain components is BEM, which names classes by block, element, and modifier, such as card, card__title, and card--large, which makes the relationship between classes clear and reduces specificity conflicts. Another approach is utility classes, where small, single-purpose classes are combined to build components, which is flexible but can make markup verbose and harder to read. Whichever architecture is chosen, it should be consistent across components, documented, and enforced, because inconsistent CSS architecture across a system creates the same confusion as inconsistent component APIs.

Accessibility in plain components relies on correct markup, because there is no framework to generate it automatically. A plain button must be a button element, a plain input must have a label, a plain tab must have tab roles and keyboard handling via JavaScript, where needed, and those details must be included in the documentation examples, so developers copy accessible markup, not just visually correct markup. That reliance on copying means documentation examples must be exemplary, not just illustrative, because they will be copied verbatim into production code, and any accessibility bug in an example will propagate to every copy. Test documentation examples with keyboard and screen readers, as you would test components themselves, because examples are part of the system.

Finally, plain HTML and CSS components can be a stepping stone to framework components, rather than a final destination. Once plain components are stable, they can be wrapped in framework components that generate the same markup, with props for variants and states, which gives teams the enforcement and convenience of a framework, while retaining the plain components' styles and accessibility. That progression, from plain to framework, is a natural evolution for many systems, and it allows the system to start simple, prove its components in real use, and then invest in framework wrappers where they add value, rather than starting with framework complexity before the components themselves are proven.

Check: What is the main risk of plain HTML and CSS components compared with framework components?

Section 33 — Building components in a framework

Building components in a framework, such as React, Vue, or a web component system, gives you enforcement, encapsulation, and reusability that plain HTML and CSS cannot provide, because the framework component generates its markup, manages its state, and exposes a typed API, rather than relying on developers to copy markup correctly. A framework button component, for example, can accept variant, size, disabled, and loading props, validate them, render the correct button element with the correct classes and attributes, handle loading state internally, and ensure that its accessible name and focus behaviour are correct, all without the caller needing to remember those details. That enforcement is the main benefit of framework components, and it is why most large design systems are built as framework components, even if they start as plain HTML and CSS.

Framework components also enable encapsulation, where a component's styles and logic are scoped to that component, reducing the risk of style conflicts or unintended interactions with other components. In some frameworks, that encapsulation is via CSS modules, scoped styles, or shadow DOM, which isolate a component's styles from the page's styles, so a change to one component does not break another.

State management in framework components is another benefit. A modal component in a framework can manage its open state, focus trapping, and Escape handling internally, exposing only a simple open prop or a trigger, rather than requiring each caller to implement focus management. An accordion component can manage which sections are open, a tab component can manage the selected tab, and a form component can manage validation state, all internally, with a clear API for the caller to control them where needed, such as via controlled props. That internal state management reduces boilerplate and errors, because common behaviours are implemented once and tested once, rather than per use, which is the same leverage as accessibility in shared components, applied to behaviour.

Framework choice also matters for a design system. A system built for one framework cannot be used directly in another framework without wrappers or rewrites, which is a limitation for organisations with multiple frameworks, or for products that use different stacks for different parts. Web components, which are framework-agnostic, offer a solution, because they work in any framework or in plain HTML, but they have their own complexities, such as styling, theming, and server rendering, which must be evaluated. A system team should choose its framework strategy based on the organisation's actual stacks, not on fashion, and should document that strategy, including how components are used in each supported stack, so teams know whether they can use the system directly or need a wrapper.

Finally, framework components must be tested as code, with unit tests for logic, such as state transitions and prop validation, with integration tests for composition, and with accessibility tests, as earlier sections discuss. Those tests should run on every change, because a framework component's logic is more complex than a plain component's styles, and regressions are more likely and more impactful. A framework component system that is well-tested, well-documented, and well-governed gives teams speed and reliability, but it requires investment in testing, in API design, and in documentation, which should be planned from the start, not added after components are already in wide use.

Check: What enforcement benefit do framework components provide over copied markup?

Section 34 — CSS architecture for systems

CSS architecture is how a design system's styles are organised, named, and scoped, so they remain maintainable as the system grows, and choosing an architecture early prevents the specificity wars and duplication that plague large stylesheets. Common architectures include BEM, which names classes by block, element, and modifier, such as card, card__title, and card--featured, which makes relationships clear and keeps specificity low, because most styles are single classes. Another approach is utility-first CSS, where small, single-purpose classes, such as text-center, p-4, and bg-primary, are combined in markup to build components, which is flexible and reduces custom CSS, but can make markup verbose and can blur the line between component and utility. A third approach is CSS-in-JS or scoped styles, where styles are written alongside component code and scoped to that component, which gives encapsulation but can make theming and global tokens harder to manage.

Each architecture has trade-offs, and the choice should be based on the team's skills, the product's needs, and the system's goals, not on popularity. BEM is explicit and framework-agnostic, but it requires discipline to name classes consistently and to avoid deep nesting.

A key principle across architectures is to keep specificity low and predictable. Specificity is the browser's rule for deciding which style wins when multiple rules apply, and high specificity, such as deeply nested selectors or id selectors, makes styles hard to override and creates brittle dependencies, where a small change in markup breaks styling. Low specificity, such as single classes, makes styles easier to override and to reason about, because the winning rule is usually the one that appears later or is more specific in a predictable way. Avoid !important, which overrides specificity entirely and creates a cascade of !important overrides that are hard to untangle, and avoid styling by element type alone, such as styling all buttons globally, because that affects buttons in unexpected places, such as inside third-party components, and makes components less predictable.

Tokens should be the foundation of any CSS architecture, because they centralise values and make themes possible, regardless of whether styles are written as BEM classes, utilities, or scoped styles. A BEM component should use token variables for colours and spacing, a utility system should generate utilities from tokens, and a scoped component should use tokens for its values, so a theme change or a brand update flows through all architectures via tokens, rather than requiring edits to each architecture's values. That token foundation is what makes a CSS architecture a system, rather than a collection of styles, because it ensures that values are shared, not duplicated, across the system.

Finally, CSS architecture should be documented, with naming conventions, file organisation, and examples, and it should be enforced with linting and code review, because architecture that is not enforced will drift, as developers invent their own naming or specificity patterns per component. Linting can check for disallowed selectors, such as ids or !important, for naming conventions, and for token usage, such as requiring variables instead of hard-coded colours, which catches drift early. A CSS architecture that is chosen deliberately, documented clearly, and enforced consistently gives a design system a maintainable foundation, while one that grows organically without architecture will become a tangled stylesheet that teams fear to change, which undermines the system's purpose.

Check: Why is low CSS specificity better for a maintainable design system?

Section 35 — Theming and dark mode in systems

Theming in a design system, as introduced earlier, is the ability to change appearance by remapping tokens, and in a system context, it requires coordination across tokens, components, and documentation, because a theme affects every component, not just colours. A system that supports dark mode, for example, must ensure that every component's colours, shadows, borders, and focus indicators work in dark mode, not just its main backgrounds and texts, because a component that was only tested in light mode may have a dark mode bug, such as a shadow that disappears, a border that becomes invisible, or a focus ring that fails contrast. That coordination is why theming should be a system-level concern, with tokens, component tests, and documentation all covering themes, rather than a per-component afterthought.

Implementing themes in a system typically involves defining semantic tokens for each theme, as earlier sections explain, and ensuring that components use only semantic tokens, not primitive values directly, so themes can remap them. If a component uses a primitive colour directly, such as grey-800, that colour will not change in dark mode, unless the component is also updated, which breaks the theme mechanism.

Dark mode in a system also requires attention to images, illustrations, and other non-token visuals, because those do not automatically adapt to themes. An illustration designed for light mode may look harsh or invisible on dark mode, and a logo designed for light mode may need a dark mode version. A system should provide guidance and, where possible, components or tokens for handling those visuals, such as image filters, alternative assets, or background treatments, rather than leaving teams to handle them ad hoc, which creates inconsistency. Similarly, third-party components or embedded content that do not support themes may need wrappers or fallbacks, so they do not break the theme experience, even if they cannot fully adapt.

Testing themes in a system requires testing every component in every theme, with automated screenshot comparisons or manual review, because a theme bug in one component can be missed if only a few components are tested. A practical approach is to render the component catalogue in each theme, and to review it, either by eye or with automated contrast and visual checks, on every theme-related change, so regressions are caught before they reach products. That catalogue review should include states, such as hover, focus, error, and disabled, in each theme, because a state that works in light mode may fail in dark mode, as earlier sections note for button states.

Finally, theme documentation should state which themes the system supports, how to switch themes, how to add a new theme, and what constraints themes must meet, such as contrast requirements and token usage rules. That documentation helps product teams use themes correctly, and helps system teams evaluate new theme requests, such as a brand theme for a new client, by checking whether the request can be met with token remapping alone, or whether it requires new components or new visuals. Theming is a powerful system feature, but it is only as good as its coordination across tokens, components, tests, and documentation, and that coordination is what makes a system-level theme reliable, rather than a collection of per-component dark mode hacks.

Check: Why must components use semantic tokens, not primitive values, for theming to work?

Section 36 — Responsive components

Responsive components are components that adapt to their container or viewport size, not merely by scaling, but by changing layout, content, or behaviour where appropriate, so they remain usable at every size. A card grid, for example, might show four columns on desktop, two on tablet, and one on phone, with card content adapting, such as hiding secondary metadata on small screens, to keep cards readable. A navigation component might show a full header on desktop, and a drawer on phone, as earlier sections discuss. A table might scroll horizontally on phone, or stack into cards, as earlier sections discuss for tables. Those adaptations are part of the component, not per-page fixes, because the same component will be used at many sizes across many pages, and its responsive behaviour should be consistent wherever it appears.

Designing responsive components requires thinking in terms of container size, not just viewport size, because a component may sit in a sidebar on desktop, where it is narrow, and full-width on phone, where it is wide, so viewport breakpoints alone do not predict its available width. Container queries, a modern CSS feature that lets styles respond to a container's size, are valuable for responsive components, because they allow a component to adapt to its actual container, rather than to the viewport, which may not match.

Content adaptation in responsive components must be handled carefully, because hiding content on small screens can hide information users need, and showing different content at different sizes can confuse users who switch devices and find different information. A better approach is to prioritise content, showing the most important content at all sizes, and progressively revealing secondary content as space allows, rather than hiding primary content on small screens. For example, a card might always show its title and primary action, and show secondary metadata only on larger screens, because title and action are essential, while metadata is supplementary. That prioritisation should be documented, so teams understand what is hidden and why, and so users who need hidden content can find it, such as via a detail view, rather than losing it entirely on small screens.

Interaction adaptation is also part of responsive components. A hover-dependent interaction, such as a tooltip on hover, must have a tap equivalent on touch, because hover does not exist on touch in the same way. A drag interaction must have a button equivalent for users who cannot drag, as earlier sections discuss for gestures.

Finally, responsive components should be tested at many sizes, not just at desktop and phone presets, because intermediate sizes often reveal layout problems, such as a grid with an awkward number of columns, or a navigation that wraps badly, that presets hide. Test by resizing the browser slowly, by testing in containers of different widths, and by testing on real phones and tablets, confirming that the component remains readable, tappable, and navigable at every size, and that its content prioritisation is sensible. A responsive component is not one that merely scales; it is one that adapts its layout, content, and interaction to serve users at every size, which is a design discipline, not just a technical feature.

Check: Why is container size a better basis than viewport size for a component's responsive behaviour?

Section 37 — Testing components

Testing components requires a layered approach, because components have visual, behavioural, accessibility, and API aspects, each of which needs different tests. Visual tests check that a component looks correct in all its variants, states, and themes, often via screenshot comparisons against a baseline, or via manual review of a component catalogue. Behavioural tests check that a component responds correctly to interaction, such as clicking, keyboard navigation, and state changes, often via unit or integration tests that simulate events and assert on outcomes. Accessibility tests check semantics, keyboard support, focus, and contrast, via automated tools and manual testing, as earlier sections discuss. API tests check that props, slots, and events work as documented, including edge cases such as invalid props or missing content, which should fail gracefully, not break the component.

Unit tests for components should focus on logic, not on implementation details, because implementation details change, while behaviour should remain stable. For example, a test for a tab component should check that activating a tab shows its panel and hides others, not that it uses a specific internal variable or class name, because the latter will break when the implementation is refactored, even if behaviour is unchanged.

Visual regression tests, which compare screenshots of components against baselines, are valuable for catching unintended visual changes, such as a colour, spacing, or layout shift caused by a token change or a CSS refactor. Those tests should be run on every component change, with baselines updated only when a visual change is intentional, and with a review step where a human approves new baselines, because an automated test that auto-updates baselines will hide regressions. Visual tests should cover variants, states, and themes, because a change that looks correct in the default state may break an error state or a dark theme, and those breaks will only be caught if those states and themes are included in the test matrix. That matrix can be large, so prioritise high-risk components and states, and use a component catalogue to render all variants systematically, rather than testing each component ad hoc.

Interaction and accessibility tests should be part of the component's test suite, not separate manual checks, where possible, by using testing libraries that simulate keyboard, focus, and screen reader semantics, and by running automated accessibility checks on rendered components. Those tests should cover tab order, focus visibility, aria attributes, and keyboard handlers, because those are the aspects most likely to regress when a component is refactored.

Finally, component tests should include usage tests, where components are tested in their common compositions, such as a form composed of inputs, or a modal composed of buttons, because a component that passes in isolation may fail when composed, due to style conflicts, focus interactions, or state dependencies. Those composition tests reveal integration problems that unit tests miss, and they should be part of the system's test suite, especially for high-use compositions. A component system that is well-tested at unit, visual, interaction, accessibility, and composition levels gives teams confidence to use components without fearing regressions, which is essential for adoption, because teams will only rely on a system they trust to remain correct as it evolves.

Check: Why should component tests focus on behaviour rather than internal implementation details?

Section 38 — Visual regression testing

Visual regression testing is the practice of comparing screenshots of components or pages against baseline screenshots, to catch unintended visual changes, and it is particularly valuable for design systems, because a small token or CSS change can affect many components, and manual review of every component on every change is impractical. A visual regression system typically renders each component in its variants, states, and themes, takes screenshots, and compares them pixel by pixel, or by perceptual similarity, against baselines from a previous version, flagging any differences for human review. Those flagged differences may be intentional, such as a new button colour, or unintentional, such as a spacing shift caused by a token change, and the human review step is essential to distinguish them, because automated comparison alone cannot judge intent.

Setting up visual regression testing requires a component catalogue, such as a storybook or a similar tool, where each component's variants are rendered in isolation, with consistent viewport sizes, fonts, and data, so screenshots are comparable across runs. Without a catalogue, visual tests must render full pages, which are harder to compare, because page content changes frequently and differences may be due to data, not design.

Visual regression tests have limitations that must be understood. They can be brittle, flagging differences due to font rendering, anti-aliasing, or environment changes, such as a different browser version, that are not real design changes, which creates noise and can cause teams to ignore test results. To reduce brittleness, visual tests should run in a consistent environment, with pinned browser versions, fonts, and screen sizes, and with thresholds that ignore tiny pixel differences, such as anti-aliasing, while flagging larger changes. They also cannot judge whether a visual change is good or bad, only that it differs, so human review remains necessary, and visual tests should be seen as a flagging mechanism, not as a pass-or-fail judgement, because a flagged difference that is intentional should be approved and become the new baseline, not treated as a failure.

Visual regression testing should also cover responsive sizes and themes, because a change that looks correct on desktop in light mode may break on phone in dark mode, and those breaks will only be caught if those sizes and themes are included in the test matrix. That matrix can become large, so prioritise: test all components at desktop size in light mode, test high-risk components at phone size and in dark mode, and expand coverage as the system matures and as resources allow.

Finally, visual regression testing should be integrated into the development workflow, running on every pull request that changes components, tokens, or styles, with results visible to reviewers, so visual changes are reviewed alongside code changes, rather than discovered after release. That integration makes visual review a normal part of code review, not a separate, easily skipped step, and it helps teams catch visual regressions early, when they are cheap to fix, rather than after they have reached users. Visual regression testing is not a replacement for design review or for manual testing, but it is a safety net that catches unintended changes that human review, focused on code, might miss, and that safety net is essential for a system used across many products.

Check: Why must visual regression differences be reviewed by a human rather than auto-approved?

Section 39 — Design handoff and collaboration

Design handoff is the process by which designs move from designers to developers, and a design system improves handoff by providing a shared language, shared components, and shared tokens, so handoff becomes a matter of composing system pieces, rather than translating a unique design from scratch per page. In a system without shared components, handoff involves designers creating mockups with custom elements, developers interpreting those mockups and building custom code, and reviewers checking whether the built page matches the mockup pixel by pixel, a process that is slow, error-prone, and prone to drift, because mockups and code diverge over time. In a system with shared components, handoff involves designers composing mockups from system components, developers implementing those compositions with system code, and reviewers checking whether the composition matches the system's guidance, which is faster, more accurate, and more maintainable, because both sides use the same pieces.

Tools for handoff, such as design files with component libraries, token exports, and specifications for spacing, colour, and type, support that shared language, but they do not replace communication. Designers and developers must still talk, especially about edge cases, such as long content, empty states, and error states, which mockups often omit, and about interactions, such as animations, focus, and responsive behaviour, which static mockups cannot fully convey.

Design tokens are a key handoff mechanism, because they are the shared values that both design tools and code use, and they should be exported from design tools to code automatically, where possible, rather than copied manually, because manual copying introduces errors and drift. When tokens are exported automatically, a colour change in a design file can flow to code via a token update, with a version bump and a changelog, rather than requiring developers to search for hard-coded colours and update them individually.

Collaboration in a design system also includes feedback loops, where developers report problems with components, such as a variant that does not meet a real need, or a token that is missing, and designers respond by improving the component or adding the token, rather than developers working around the system in isolation. Those feedback loops should be formalised, with channels for reporting, with triage and prioritisation, and with a roadmap that shows how feedback is being addressed, because without them, teams will feel unheard and will stop using the system, building their own components instead.

Finally, handoff and collaboration should be measured, not assumed. Track how long handoff takes, how many handoff errors occur, such as built pages that do not match designs, and how satisfied designers and developers are with the process, because those metrics reveal whether the system is actually improving handoff, or merely adding a layer of process without benefit. If handoff is not improving, investigate why: are components hard to find, is documentation unclear, are tokens out of sync, or is communication lacking? Those investigations, and the improvements they lead to, are part of system governance, and they ensure that the system serves its users, designers and developers alike, rather than serving only its own existence.

Check: Why does a shared component library make design handoff faster and more accurate?

Section 40 — Governance and contributions

Governance is how a design system is managed, decided, and evolved, and it is essential because a system without governance will either stagnate, as no one updates it, or fragment, as everyone updates it in different directions. Governance includes who can propose changes, how changes are reviewed, who approves them, how they are versioned and communicated, and how contributions from product teams are handled, because a system used by many teams cannot be managed by a single team in isolation, nor can it be a free-for-all where any team changes shared components without review. A clear governance model, documented and communicated, gives teams confidence that the system will evolve predictably, and that their contributions will be considered fairly, which encourages adoption and contribution, rather than workarounds.

A common governance model is a core system team that owns the system, reviews contributions, and makes final decisions, with product teams contributing components, tokens, or guidance through a defined process, such as a proposal, a design review, a code review, and a trial period. That model balances central quality control with distributed innovation, because product teams, who encounter real needs first, can propose solutions, while the core team ensures those solutions meet system standards for accessibility, consistency, and maintainability, before they are adopted system-wide.

Contribution processes should be clear, lightweight, and supportive, because a process that is too heavy will discourage contributions, while a process that is too light will allow low-quality contributions to enter the system. A good process might include a proposal template, asking what problem the contribution solves, what alternatives were considered, and how it will be tested, followed by a design and code review, followed by a trial in one product, followed by adoption system-wide if the trial succeeds. That process should be documented, with examples of successful contributions, and with support from the core team, such as pairing or office hours, because contributors are often product developers with limited time, and they need guidance to meet system standards, not just a set of rules to follow alone.

Decision-making in governance should be transparent, with recorded decisions, rationales, and alternatives considered, so teams understand why a proposal was accepted or rejected, and can learn from those decisions for future proposals. Transparency also builds trust, because teams are more likely to accept a rejection they understand than one that feels arbitrary, and they are more likely to contribute again if they feel their proposals were considered fairly, even when rejected.

Finally, governance includes maintenance, such as auditing components for usage, for accessibility, and for outdated patterns, and removing or deprecating components that are no longer used or that have been superseded, because a system that only grows, without pruning, becomes bloated and hard to learn. Regular audits, with usage data, help the core team decide what to keep, what to improve, and what to remove, with migration support for removals, as earlier sections discuss for breaking changes. Governance is therefore not just about approving new things; it is about curating the system over time, keeping it focused, high-quality, and relevant, which is the work that makes a design system last beyond its initial launch.

Check: Why should product teams be able to contribute to a design system, rather than only a core team building it?

Section 41 — Adoption and migration to a system

Adopting a design system in an existing product is a migration, not a switch, because existing pages already use custom components and styles, and replacing them all at once would be risky, disruptive, and expensive. A better approach is incremental adoption, where new pages and features use the system from the start, while existing pages migrate gradually, prioritising high-traffic, high-value, or high-maintenance pages first, and leaving low-traffic pages for later, or for when they are next redesigned. That incremental approach spreads cost over time, reduces risk, because each migration is small and testable, and allows teams to learn the system on new work, where its benefits are clearest, before applying it to legacy work, where its constraints may feel more burdensome.

Migration planning should start with an audit of existing components, to identify which custom components correspond to system components, which are unique and may need to become system components, and which are one-offs that should be replaced or removed. That audit gives a map of the migration, with estimates of effort per page or per component, and it reveals patterns, such as a custom button used in fifty places, which should be migrated early, because its migration gives a large consistency benefit, versus a custom widget used once, which can wait.

Support for migrating teams is essential, because migration is work on top of their normal feature work, and without support, it will be deprioritised indefinitely. Support can include migration guides, codemods, pairing with system team members, and dedicated migration time, such as a sprint or a percentage of capacity allocated to migration, rather than expecting teams to migrate in their spare time, which does not exist. It can also include a clear definition of done for migration, such as a page uses only system components, passes accessibility checks, and matches system guidance, so teams know when a page is considered migrated, rather than partially migrated in a way that leaves a mix of system and custom components that is harder to maintain than either alone.

Measuring adoption is also important, because it shows whether the system is actually being used, and where it is not, so the system team can investigate and improve. Adoption metrics might include the percentage of pages using system components, the percentage of new features built with the system, the number of custom components remaining, and team satisfaction with the system, measured via surveys or interviews.

Finally, adoption should be celebrated and communicated, because migration is often invisible work, and teams that migrate early and well should be recognised, with examples of their migrated pages shared as models for other teams. That recognition encourages other teams to migrate, and it builds a community around the system, where teams share tips, components, and patterns, rather than each team migrating in isolation. Adoption is a social process as much as a technical one, and treating it as such, with support, measurement, recognition, and community, is what turns a design system from a set of components into a shared way of building a product, which is its ultimate goal.

Check: Why is incremental adoption safer than replacing all custom components at once?

Section 42 — Measuring a design system's success

Measuring a design system's success is necessary to justify its investment, to guide its evolution, and to demonstrate its value to stakeholders who fund it, because a system without evidence of value will struggle for resources and support. Success metrics should cover several dimensions: efficiency, consistency, quality, and satisfaction, because a system that is efficient but inconsistent, or consistent but low-quality, is not truly successful. Efficiency metrics might include time to build a new page or feature with the system versus without it, or the number of lines of custom code avoided by using system components. Consistency metrics might include the number of custom components remaining, or visual consistency scores from audits. Quality metrics might include accessibility violations, bugs, or performance, comparing system-built pages with custom-built pages. Satisfaction metrics might include designer and developer surveys, measuring how easy the system is to use, how well it meets needs, and how likely teams are to recommend it.

Choosing metrics requires care, because metrics can be gamed or misinterpreted. Time to build, for example, may decrease with a system, but it may also decrease because teams become more experienced, or because features become simpler, so attributing the decrease solely to the system requires careful comparison, such as comparing similar features built with and without the system, or tracking time over a longer period.

A useful approach is to define success criteria before building or adopting a system, such as reducing time to build a standard page by a certain percentage, reducing accessibility violations by a certain amount, or achieving a certain adoption rate within a year, and then to measure against those criteria regularly, reporting progress transparently. Those criteria give the system team a target and give stakeholders a way to judge whether the investment is paying off, without requiring the system team to prove value from scratch on every review. Criteria should be realistic and should account for the system's costs, such as maintenance and migration, because a system that saves time on new pages but costs more in maintenance than it saves is not a net success, even if its efficiency metric looks good in isolation.

Metrics should also be used to guide improvement, not just to justify existence. If adoption is low for a particular component, investigate why, as earlier sections discuss for adoption, rather than merely reporting low adoption. If accessibility violations remain high despite system use, investigate whether components are accessible but misused, or whether components themselves have bugs, because those two causes require different fixes.

Finally, communicate metrics and their interpretation honestly, including limitations and costs, because overstating a system's success undermines trust when stakeholders discover its limitations later. Report both benefits and costs, such as time saved versus time spent maintaining the system, and report qualitative feedback alongside quantitative metrics, so stakeholders get a full picture, not just a favourable number. A system team that measures honestly, acts on its measurements, and communicates transparently will earn trust and support, even when metrics are mixed, because stakeholders will see a team that is learning and improving, rather than one that is merely promoting its own work.

Check: Why should design system metrics be interpreted together rather than in isolation?

Section 43 — Common pitfalls and over-abstraction

Design systems fail in predictable ways, and recognising those pitfalls early can prevent a system from becoming a burden rather than a benefit. One common pitfall is over-abstraction, where components are made so generic, with so many props and options, that they become hard to understand and hard to use, because every use requires configuring many options, and the component's behaviour becomes unpredictable. An over-abstracted button, for example, might accept any colour, any size, any icon position, and any behaviour, which sounds flexible, but in practice means teams must specify everything per use, defeating the system's purpose of providing sensible defaults and consistency. A better approach is to provide a small set of well-chosen variants, with clear defaults, and to allow composition for unusual cases, rather than making every component infinitely configurable.

Another pitfall is building for hypothetical needs, rather than real ones, where a system team builds components for cases that might arise, based on speculation, rather than for cases that teams actually face, based on real products. Those hypothetical components often go unused, because they solve problems no one has, while real needs go unmet, because the system team spent time on speculation instead of on listening to teams.

A third pitfall is neglecting maintenance, where a system is launched with fanfare, but no one is assigned to maintain it, so components become outdated, documentation drifts, and teams stop trusting it, because it no longer matches the product's needs or the latest accessibility standards. Maintenance requires dedicated time, ownership, and funding, not just initial building, and it should be planned from the start, with a core team or a rotating responsibility, with regular audits, and with a roadmap for updates. A system without maintenance will decay, and its decay will be gradual, so it may not be noticed until teams have already worked around it extensively, at which point reviving it is harder than building it anew.

A fourth pitfall is imposing a system without involving its users, where a central team designs components in isolation, without input from product teams, and then mandates their use, which breeds resentment and workarounds, because the components do not meet real needs or do not fit real workflows. Involving users, through interviews, trials, contributions, and feedback loops, as earlier sections discuss for governance, prevents that pitfall, because it ensures components are designed for real use, and it builds ownership among product teams, who feel they helped build the system, rather than having it imposed on them.

Finally, a pitfall is measuring success by the system's size, such as the number of components, rather than by its usefulness, such as adoption, efficiency, or quality, which encourages building many components, even if they are unused, rather than building few, high-quality components that teams actually use. A system with twenty well-used components is more successful than one with two hundred rarely used ones, because the former is focused and maintainable, while the latter is bloated and hard to learn. Resist the temptation to grow the system for its own sake, and instead grow it based on evidence of need, with pruning of unused components, as earlier sections discuss for governance, so the system remains a tool for building products, not a monument to its own completeness.

Check: Why does an over-abstracted component defeat the purpose of a design system?

Section 44 — Starting small: a minimum viable system

Starting a design system does not require building a full system at once; in fact, starting small, with a minimum viable system, is often the best approach, because it delivers value quickly, learns from real use, and grows based on evidence, rather than speculation. A minimum viable system might include only tokens for colour, type, and spacing, a few core components, such as buttons, inputs, and cards, and basic documentation for those pieces, with a process for adding more as needs emerge. That small set, if well-chosen, covers a large share of most pages' needs, because buttons, inputs, and cards appear everywhere, and tokens ensure consistency even for custom elements built outside the system, because they use the same values.

Choosing the initial components for a minimum viable system should be based on usage and on pain. Audit existing pages to find which elements appear most frequently, and which cause the most inconsistency or bugs, such as buttons with many variations, or form fields with accessibility problems, and start with those, because fixing them gives the largest benefit for the smallest effort.

A minimum viable system should also include the processes that will allow it to grow, even if its component set is small. Those processes include token governance, versioning, documentation, contribution, and testing, in lightweight forms, such as a simple changelog, a documentation page per component, and a review checklist, rather than full, heavyweight processes that would slow a small system down. Lightweight processes are sufficient for a small system, because its scope is small, and they can be strengthened as the system grows, rather than being imposed in full from the start, which would be disproportionate and would discourage early adoption. The key is to have processes, not to have perfect processes, because processes can evolve, while a system without any process will drift from the start.

Documentation for a minimum viable system should be concise but complete for its scope: each token and component should have a description, examples, API, variants, states, and guidance, as earlier sections discuss, but only for the components in scope, not for hypothetical future components. That completeness for a small scope is better than incompleteness for a large scope, because teams can rely on the documented components fully, rather than guessing about undocumented ones.

Finally, a minimum viable system should be launched with a pilot, where one team or one product uses it for a real feature, with support from the system team, and with feedback collected and acted on, before it is rolled out more widely. That pilot reveals problems with components, documentation, and processes, while the system is still small and easy to fix, rather than after it has been adopted widely and problems are harder to fix. It also creates a success story and a champion team that can advocate for the system to other teams, which is more persuasive than a system team promoting its own work. Starting small, with a pilot, with lightweight processes, and with a focus on real, frequent needs, is the most reliable path to a design system that grows into a valuable, adopted system, rather than a large, unused one.

Check: Why should a minimum viable system start with frequently used components rather than complex ones?

Section 45 — Design systems and accessibility at scale

Accessibility at scale is one of the strongest arguments for a design system, because a system allows accessibility to be built once, tested once, and reused everywhere, rather than being implemented per page, with varying quality. When a button component has correct semantics, focus, and contrast, every button built with it inherits those qualities, without each team needing to be accessibility experts. When a form component has correct labels, error handling, and keyboard support, every form built with it inherits those qualities. That leverage means a system team with accessibility expertise can raise the accessibility of an entire product, by fixing components, rather than by fixing pages one by one, which is far more efficient and more reliable, because component fixes propagate automatically to all uses, while page fixes must be repeated per page.

Achieving accessibility at scale requires that accessibility be part of the system's definition of done for every component, not an optional check at the end. A component should not be considered complete until it has been tested with keyboard, with screen readers, with contrast checks, and with reduced motion and increased text size, as earlier sections discuss, and until its accessibility behaviours are documented, so teams using it know what it handles and what remains their responsibility, such as providing meaningful labels or alt text.

Accessibility at scale also requires that the system team stays current with accessibility standards and with assistive technology, because standards evolve and new patterns emerge, and a system that was accessible when built may become outdated if it is not updated. Regular accessibility audits of the system, with real assistive technology and with users who rely on it, help the system team stay current, and updates to components, such as improving focus indicators or adding new aria patterns, should be versioned and communicated, with migration support, as earlier sections discuss for breaking changes, because accessibility improvements can be breaking if they change markup or behaviour, but they are still worth making, with proper support, because they improve the product for users who need them most.

A related challenge is that accessibility at scale can create a false sense of security, where teams assume that because they use system components, their pages are automatically accessible, without considering how components are composed, what content they provide, or what custom code they add alongside system components. A page built from accessible components can still be inaccessible, if its heading order is wrong, if its images lack alt text, if its custom interactions are not keyboard accessible, or if its content is structured poorly.

Finally, accessibility at scale should be measured, with metrics such as the number of accessibility violations in system-built pages versus custom-built pages, the percentage of components that pass accessibility tests, and feedback from users who rely on assistive technology, because those metrics show whether the system's accessibility leverage is actually working, and where it needs improvement. If system-built pages still have many violations, investigate whether components are accessible but misused, or whether components themselves have gaps, because those causes require different fixes, as earlier sections discuss for metrics.

Check: Why can a page built from accessible components still be inaccessible?

Section 46 — Systems for multiple products and brands

A design system that serves multiple products, or multiple brands within one organisation, faces additional challenges, because it must provide consistency where consistency matters, and flexibility where products or brands legitimately differ. Consistency matters for core interactions, such as buttons, forms, and navigation, where users benefit from learning once and applying everywhere, and for accessibility, where shared components ensure shared quality. Flexibility matters for brand identity, such as colours, typefaces, and logos, where different brands should look distinct, and for product-specific patterns, such as a dashboard's charts versus a store's product cards, where a single component set cannot cover all needs without becoming bloated. Balancing those two needs is the central challenge for multi-product systems.

A common architecture for multi-product systems is a core system, with shared tokens, components, and patterns that all products use, plus product-specific layers, where each product or brand extends or overrides the core for its own needs, such as brand colours via theme tokens, or product-specific components that are not shared. That layered architecture keeps the core small and maintainable, while allowing products to differ where they must, without forking the core into separate, diverging systems per product, which would defeat the system's purpose.

Theming, as discussed earlier, is the primary mechanism for brand flexibility in a multi-product system. Each brand can define its own theme, remapping semantic tokens to brand colours, typefaces, and spacing, while using the same components, so products look distinct but behave consistently. That theming must be tested per brand, because a component that works in one brand's theme may fail contrast or legibility in another's, if the brand's colours are too light or too similar, so theme testing should be part of the system's test matrix, with each brand's theme rendered in the component catalogue and reviewed.

Product-specific components in a multi-product system should be documented and catalogued alongside core components, with clear labelling of which are core and which are product-specific, so teams do not mistakenly use a product-specific component in another product where it does not belong, or duplicate a core component because they did not know it existed. That catalogue, with ownership and usage information, helps teams find the right component, whether core or product-specific, and helps the core team identify product-specific components that should be promoted to core, because they are used in multiple products or solve a general problem.

Finally, multi-product systems require coordination across product teams, with regular communication, shared roadmaps, and joint decision-making, because a change to the core affects all products, and product teams need advance notice and migration support, as earlier sections discuss for breaking changes. That coordination can be achieved through a system council, with representatives from each product, or through regular system reviews, where core changes are proposed, discussed, and approved with product input, rather than imposed by the core team alone. That shared governance builds trust and adoption across products, because each product feels it has a voice in the system's evolution, rather than being subject to a system designed for another product's needs. Multi-product systems are more complex to govern, but they offer greater leverage, because a single accessibility fix or a single new component can benefit many products at once, which is a powerful argument for investing in them.

Check: Why should a multi-product system have a core layer plus product-specific layers, rather than one layer for all?

Section 47 — Keeping a system alive

Keeping a design system alive means maintaining it over time, as products evolve, as standards change, and as teams change, so it remains relevant, trusted, and used, rather than becoming a snapshot of a past design that teams ignore. Maintenance includes updating components for new accessibility standards, for new browser features, and for new brand or product needs, with versioning and migration support, as earlier sections discuss. It includes pruning unused components, updating documentation, and fixing bugs reported by teams, with prioritisation based on usage and impact, rather than on who shouts loudest. It also includes onboarding new team members, both to the system team and to product teams, so knowledge of the system does not leave when individuals leave, which is a common cause of system decay in organisations with high turnover.

A key maintenance practice is regular auditing, where the system team reviews component usage, documentation accuracy, accessibility, and performance, and identifies components that are outdated, duplicated, or unused, and documentation that is inaccurate or incomplete. Audits should be scheduled, such as quarterly, rather than done only when problems become obvious, because regular audits catch small drifts before they become large problems, such as a token that is no longer used, or a component that has been superseded but not deprecated, which confuses teams about which component to use.

Another maintenance practice is to keep the system team connected to product teams, through regular check-ins, usage data, and feedback channels, because a system team that loses touch with its users will build components that no longer meet real needs, even if they were correct when first built. Product needs change, new patterns emerge, and old patterns become obsolete, and only by staying connected can the system team learn about those changes in time to respond. That connection can be maintained through office hours, through embedding system team members in product teams for periods, or through a community of practice where system users share tips and problems, all of which help the system team learn and help product teams feel supported, which encourages continued adoption.

Funding and staffing for maintenance are also essential, because a system without dedicated resources will decay, however good its initial launch, as earlier sections warn for the pitfall of neglecting maintenance. System maintenance should be funded as an ongoing product, with a roadmap, with metrics, and with stakeholder support, not as a one-off project that ends after launch, because its value accrues over time, through reuse, consistency, and quality, and that value requires ongoing investment to sustain.

Finally, keeping a system alive requires celebrating its successes and learning from its failures, with case studies, with recognition for contributors, and with retrospectives after major releases or migrations, as earlier sections discuss for governance and for adoption. Those practices build a culture around the system, where it is seen as a shared asset that teams contribute to and benefit from, rather than as a burden imposed by a central team, and that culture is what sustains a system beyond its initial champions, through staff changes and product shifts. A system that is maintained, audited, connected to its users, funded, and celebrated will live and grow, while one that is launched and left alone will fade, however good its initial components were.

Check: Why should design system audits be scheduled regularly rather than done only when problems appear?

Section 48 — Design systems and performance

Design systems affect performance in several ways, both positive and negative, and managing that impact is part of system design, because a system that makes pages slow will be resented, however consistent it makes them look. Positively, a system can improve performance by encouraging reuse, which reduces duplicated code, by providing optimised components, such as images with correct sizes or lists with efficient rendering, and by centralising performance best practices, such as lazy loading or code splitting, in components, so teams benefit without needing to implement those practices per page. Negatively, a system can harm performance if it is bloated, with many unused components included in every page's bundle, or if its components are heavy, with large dependencies, unoptimised images, or expensive animations, which slow pages that use them, even for simple features.

Bundle size is the most direct performance impact. If a design system is delivered as a single bundle containing all components, and a page uses only a few, the page will load unnecessary code, which wastes bandwidth and parsing time, especially on slow connections or phones.

Component performance also matters at runtime, not just at load time. A component that re-renders frequently, that animates expensive properties, or that measures layout on every interaction, can cause jank or high processor usage, especially when used many times on a page, such as a list of cards each with its own animation or data fetching. System components should be designed for performance, with memoisation where appropriate, with transform and opacity animations rather than layout animations, as earlier chapters discuss, and with efficient rendering, such as virtualisation for long lists, where only visible items are rendered, rather than rendering thousands of items at once. Those performance characteristics should be documented, so teams know when a component is suitable for large lists or for frequent updates, and when a different pattern, such as pagination or virtualisation, is needed.

Images, fonts, and other assets in a system also affect performance. A system that includes many web fonts, or large icon sets, or high-resolution images, can slow pages that use it, even if those assets are not all needed, so assets should be optimised, subsetted, and loaded on demand, where possible. Icon systems, for example, should load only the icons used, via a sprite or via individual SVG imports, rather than loading a full icon font with hundreds of unused icons.

Finally, performance should be measured for the system as a whole, with benchmarks for common pages built with the system, compared with benchmarks for similar pages built without it, to show whether the system helps or harms performance in practice, not just in theory. If the system harms performance, investigate whether the cause is bundle size, component runtime, or assets, and fix the specific cause, rather than abandoning the system, because performance problems in a system are often fixable with tree-shaking, optimisation, or lazy loading, and fixing them benefits all products using the system, which is leverage similar to accessibility leverage. A system that is performant, as well as consistent and accessible, will be adopted more readily, because teams will see it as helping, not hindering, their pages' speed, which is a key user experience metric.

Check: Why should a design system support tree-shaking or modular imports?

Section 49 — Ethics, inclusion, and representation in systems

Design systems shape how products represent people, cultures, and identities, through their imagery, language, icons, and default content, and those choices have ethical and inclusion implications that system teams must consider deliberately, rather than by default. For example, a system that provides avatar components with only a narrow range of skin tones, or illustration components that only show one type of person, will produce products that exclude or misrepresent many users, even if individual teams do not intend exclusion, because they are using system defaults. Similarly, a system whose language guidance uses gendered terms, or whose icons rely on cultural assumptions, such as a mailbox icon that is unfamiliar in some cultures, can exclude users who do not share those assumptions. System teams must therefore audit their components, imagery, and language for inclusion, and provide diverse, representative defaults, with guidance on how to use them inclusively.

Language in a system, including component labels, error messages, and documentation, should be plain, respectful, and inclusive, avoiding jargon, stereotypes, and terms that exclude or offend, and using person-first or identity-first language as appropriate to the context and to community preferences, which vary and should be researched, not assumed. Error messages, for example, should blame the system, not the user, such as "We could not save your changes" rather than "You entered invalid data", because blaming language makes users feel at fault for system problems.

Imagery and icons in a system should also be audited for representation and for stereotypes. Illustrations should show diverse people, in diverse roles, without reinforcing stereotypes, such as showing only men as leaders or only women as caregivers, because those representations affect how users see themselves in a product and how others see them.

Accessibility is also an ethical issue, as earlier sections discuss, because excluding users with disabilities from a product is not merely a technical failure, but a failure to serve all users equitably. A system that prioritises accessibility, with components that are keyboard accessible, screen reader friendly, and contrast compliant, and with guidance that teaches teams to build accessible pages, not just accessible components, serves users equitably, while a system that treats accessibility as optional excludes users who depend on it.

Finally, system teams should consider the environmental and social impact of their components, such as performance, which affects users on slow connections or low-end devices, who are often in lower-income contexts, and data usage, which affects users with limited data plans. A system that is heavy, with large bundles and many assets, disproportionately harms those users, while a lightweight, performant system serves them better, which is an inclusion issue as well as a technical one. Similarly, a system that requires the latest browsers or devices excludes users who cannot afford them, so compatibility and progressive enhancement, where core functionality works on older devices, should be part of system design. Ethics in design systems is therefore not an abstract concern, but a practical one, about who the system's defaults serve, who they exclude, and how the system team can broaden its defaults to serve more people more fairly.

Check: Why is a system's default imagery an inclusion issue, not just an aesthetic choice?

Section 50 — Design systems and reusable UI in one paragraph

A design system is a shared set of tokens, components, patterns, and guidance, governed as a product in its own right, that lets many teams build consistent, accessible, high-quality interfaces without reinventing the same pieces for every page. Tokens, from primitive values to semantic meanings to component-specific values, are its foundation, giving a single source of truth for colour, type, spacing, radius, and shadow, and enabling themes, such as dark mode, by remapping semantic tokens without changing component code. Components, from buttons and inputs to cards, modals, tables, and navigation, are its building blocks, each with variants for purpose, states for every condition, a clear API of props and slots, and accessibility built in, so a fix to a component improves every page that uses it, while a bug in a component harms every page that uses it, which is why component quality, testing, and documentation matter disproportionately.

Beyond individual components, this chapter has covered the practices that make a system work at scale: layout components for structure, icon systems for visual language, composition for building complex interfaces from simple pieces, and guidance, with do and do-not examples, that teaches judgement, not just syntax. It has covered versioning and migration, with semantic versions, deprecation, and migration support that make change predictable and manageable, and governance and contributions, where a core team and product teams collaborate, with transparent decisions, to evolve the system based on real needs, rather than speculation or imposition. It has covered adoption, measurement, and pitfalls, from over-abstraction to neglecting maintenance, and the value of starting small, with a minimum viable system, a pilot, and lightweight processes that grow with evidence, rather than launching a large, unused system all at once.

Carry forward a simple principle for any reusable UI you build, whether it is a full system or a single component: make common cases easy, uncommon cases possible, and incorrect cases hard, with tokens for values, variants for purpose, states for every condition, and documentation that shows correct use and warns against misuse. Test components in all variants, states, themes, and sizes, with keyboard, screen readers, and real content, and measure their adoption and impact, acting on feedback rather than assuming success. A system built that way will not merely make interfaces look consistent; it will make them faster to build, easier to maintain, more accessible to all users, and more trustworthy as a product, because consistency, accessibility, and quality, when built into shared pieces, compound across every page that uses them, which is the enduring value of design systems and reusable UI. When you start your own system, begin with the pieces your product actually repeats, prove them on one real feature, and let evidence, not ambition, decide what to add next, because a small system that teams trust will grow, while a large system they distrust will not.

Check: In your own words, why is a shared component's quality more important than a one-off component's quality?

Practical task

Build a small design system starter for a fictional product: define tokens for colour, type, spacing, and radius in CSS custom properties, build button, input, and card components using only those tokens, and document each component with an example, its variants and states, and a do and do-not note. Then test it: use the components to build two different pages, switch the tokens to a dark theme, check contrast and keyboard navigation in both themes, and fix any component that breaks, looks inconsistent, or fails accessibility in either theme.

Chapter questions

  1. A team builds a new button variant for every page instead of using system variants. What problems does that create, and what would you advise?
  2. A component's dark mode fails contrast for its error state, although its light mode passes. What system-level cause is likely, and how would you fix it?
  3. Explain the difference between a primitive token, a semantic token, and a component token, with an example of each.
  4. A breaking change to a widely used component is planned. What migration support should the system team provide?
  5. A design system has 200 components but low adoption. What is likely wrong, and what would you investigate first?