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.