Skip to content

Design Systems

This lesson explains how to build a design system with Web Components, including component tiers, public APIs, design tokens, documentation, and distribution across multiple applications and frameworks.

What Is a Web Components Design System?

A design system is a shared set of UI standards: components, tokens, patterns, documentation, and guidelines. When built with Web Components, the same button, input, dialog, and card can work in React, Vue, Angular, Astro, and plain HTML without rewriting each primitive per framework.

Web Components are especially strong for design systems because Shadow DOM encapsulates styles, custom elements provide a stable HTML API, and ES Modules make libraries easy to publish and import.

Layer Examples
Design tokens Colors, spacing, typography, radius
Primitives Button, input, badge, icon
Patterns Form field, modal, data table shell
Documentation Usage, props, events, accessibility
Guidelines Content, layout, accessibility rules
Distribution NPM package, CDN, internal registry

Basic Design System Usage

<link rel="stylesheet" href="@company/design-system/tokens.css" />
<script type="module" src="@company/design-system"></script>

<app-button variant="primary">Save</app-button>
<app-text-field label="Email" name="email"></app-text-field>
<app-alert type="success">Profile updated.</app-alert>

Consumers import tokens and the registration entry once, then use custom tags like standard HTML. The design system owns behavior, styling, and accessibility inside each component.

Component Tiers

Tier Purpose Example
Primitives Smallest reusable building blocks <app-button>
Composites Combine primitives into common UI <app-search-field>
Layouts Structure pages and regions <app-page-shell>
Domain widgets Product-specific but shared UI <app-pricing-card>

Keep the core design system focused on primitives and a small set of composites. Product-specific widgets can live in a separate package that depends on the base library.

Designing the Public Component API

Each component should expose a small, predictable contract: attributes for simple config, properties for complex values, slots for content, and custom events for output.

// app-button public API
// Attributes: variant, size, disabled
// Slots: default
// Events: click (native), app-press (semantic custom event)
// CSS tokens: --app-button-bg, --app-button-color
<app-button
  variant="primary"
  size="lg"
  disabled
>
  Publish
</app-button>

Document every attribute, property, slot, event, and CSS token. Stable APIs are what make a design system trustworthy across teams.

Design Tokens in the System

Tokens are the visual foundation of the system. Publish a shared tokens.css file and reference tokens inside every shadow component. See the Design Tokens lesson for implementation details.

:root {
  --color-primary: #2563eb;
  --color-danger: #dc2626;
  --space-sm: 0.5rem;
  --space-md: 1rem;
  --radius-sm: 0.375rem;
}

Design System Package Structure

@company/design-system/
├── src/
│   ├── tokens/
│   │   └── index.css
│   ├── button/
│   │   ├── app-button.js
│   │   └── app-button.test.js
│   ├── text-field/
│   │   └── app-text-field.js
│   └── index.js
├── docs/
├── package.json
└── README.md
// src/index.js
import "./tokens/index.css";
import "./button/app-button.js";
import "./text-field/app-text-field.js";

export { AppButton } from "./button/app-button.js";
export { AppTextField } from "./text-field/app-text-field.js";

One folder per component, a single registration entry, shared tokens, tests beside source files, and docs for consumers.

Building with Lit

Many design systems use Lit because it reduces boilerplate while still producing real Web Components. Lit works well for tokens, shadow styles, and reactive properties at scale.

import { LitElement, html, css } from "lit";

export class AppButton extends LitElement {
  static properties = {
    variant: { type: String },
    disabled: { type: Boolean }
  };

  static styles = css`
    :host {
      --btn-bg: var(--color-primary, #2563eb);
    }
    button { background: var(--btn-bg); }
  `;

  render() {
    return html`
      <button ?disabled=${this.disabled}>
        <slot></slot>
      </button>
    `;
  }
}

customElements.define("app-button", AppButton);

Documentation and Discovery

  • Document attributes, properties, slots, events, and CSS tokens.
  • Show live examples for each component state.
  • Include accessibility notes and keyboard behavior.
  • Provide copy-paste HTML for framework and plain HTML usage.
  • Track breaking changes in a changelog.
  • Use Storybook, Web Components docs tools, or a custom docs site.
## app-button

### Attributes
- `variant`: primary | secondary | danger
- `size`: sm | md | lg
- `disabled`: boolean

### Events
- `app-press`: dispatched on activation with `detail: { source }`

Accessibility in Design Systems

Design system components are used everywhere, so accessibility must be built in by default: focus styles, keyboard support, ARIA roles, labels, and color contrast from tokens.

// Form controls should use ElementInternals
static formAssociated = true;

constructor() {
  super();
  this.internals = this.attachInternals();
  this.internals.role = "switch";
}

Test primitives with keyboard-only navigation, screen readers, and automated tools such as axe-core before publishing new versions.

Testing Strategy

Test Type What to Verify
Unit tests Properties, events, rendering logic
Accessibility tests Roles, labels, contrast, keyboard support
Visual regression Token and style consistency
Integration tests Usage inside React, Vue, or plain HTML pages

Framework-Agnostic Distribution

A Web Components design system can power multiple apps at once. Framework teams import the package once and use the custom tags in their templates. See Framework Integration for React, Vue, and Angular patterns.

// React app entry
import "@company/design-system";
import "@company/design-system/tokens.css";

Optional framework wrappers can improve TypeScript and property binding, but the Web Component should remain the source of truth.

Versioning and Releases

  • Use semantic versioning for the npm package.
  • Treat attribute, event, and token renames as breaking changes.
  • Publish release notes with migration steps.
  • Deprecate APIs before removing them when possible.
  • Keep a stable core of primitives with slow change velocity.
{
  "name": "@company/design-system",
  "version": "2.4.0",
  "type": "module",
  "exports": {
    ".": "./dist/index.js",
    "./tokens.css": "./dist/tokens/index.css"
  }
}

Web Components vs Framework Design Systems

Topic Web Components DS Framework DS
Portability Works across frameworks Tied to one ecosystem
Encapsulation Shadow DOM built in Varies by implementation
Distribution npm + custom elements npm + framework imports
Best fit Multi-app organizations Single-framework products

Composable Design System Example

<app-card>
  <h2 slot="title">Account Settings</h2>

  <app-text-field
    label="Display name"
    name="displayName"
    value="Alex"
  ></app-text-field>

  <app-alert type="info" slot="footer">
    Changes may take a minute to appear.
  </app-alert>

  <app-button slot="footer" variant="primary">
    Save
  </app-button>
</app-card>

Small primitives compose into larger patterns without each team rebuilding the same UI in every application stack.

When a Web Components Design System Makes Sense

  • Multiple apps use different frontend frameworks.
  • You need one source of truth for brand and UI behavior.
  • Teams want encapsulated styles that do not leak globally.
  • You publish embeddable widgets for external products.
  • You are building a long-lived platform with many teams.
  • You want CMS, static, and SPA pages to share the same UI.

Design System Best Practices

  • Start with a small set of high-quality primitives.
  • Keep public APIs stable and well documented.
  • Centralize tokens and publish them with the package.
  • Build accessibility into primitives from day one.
  • Test components in plain HTML and at least one framework.
  • Version carefully and communicate breaking changes clearly.
  • Prefer composition with slots over oversized monolithic widgets.

Common Design System Mistakes

  • Shipping too many components before primitives are solid.
  • Undocumented attributes, events, and CSS tokens.
  • Hardcoding styles instead of using shared tokens.
  • Building framework-specific logic into core primitives.
  • Skipping keyboard and screen reader support.
  • No visual or accessibility regression testing.
  • Breaking public APIs without migration guidance.

Key Takeaways

  • Web Components are a strong foundation for shared design systems.
  • Primitives, tokens, docs, and distribution work together as one system.
  • Stable HTML APIs matter more than framework-specific wrappers.
  • Shadow DOM and tokens enable encapsulated, themeable UI.
  • Accessibility and testing should be part of every primitive.
  • Good design systems reduce duplication across teams and apps.

Pro Tip

Launch your design system with five excellent primitives — button, input, checkbox, alert, and dialog — before adding dozens of partial components. Quality and consistency at the base layer matter more than catalog size.