The design system is the product's visual and interaction contract. Tokens and components beat screen one-offs. Code and Figma (if any) must tell the same story.
Visual consumption in apps: better-web-interface. Identity and anti-generic: frontend-design. React component API: react-component-engineering. Living documentation: storybook-docs. Native mobile: better-mobile-interface.
Layers
Foundations (tokens) → Primitives → Components → Patterns → Product screens
| Layer | Examples | Rule |
|---|---|---|
| Foundations | color, type, space, radius, shadow, z-index, motion | No UI; tokens only |
| Primitives | Button, Input, Checkbox, Icon | Minimal API, highly reusable |
| Components | Field, Modal, DataTable | Compose primitives |
| Patterns | LoginForm, PageHeader, EmptyState | Product recipes, still reusable |
Don't skip layers: a product screen doesn't redefine padding: 13px if the token is space.16.
Foundations (tokens)
Define semantic roles, not just raw palettes:
- Color:
background,surface,text,border,accent, feedback… - Typography: families, sizes, weights, named line-heights
- Space: 4/8 scale
- Radius / border / elevation
- Motion: duration/easing
- Breakpoints
Format: CSS variables, Style Dictionary, JS/TS theme, or tokens from the project's toolchain — preserve what already exists.
:root {
--color-bg: #f7f7f5;
--color-surface: #ffffff;
--color-text: #121212;
--color-accent: #0b6e4f;
--space-4: 1rem;
--radius-md: 8px;
}
- Themes (light/dark / brand): they swap values, not the role names.
- Document aliases (
accent→ CTA) so design and eng use the same language.
Components
- One job per component; variants via discriminated props or composition.
- Required states: default, hover, focus-visible, disabled; + loading/error when applicable.
- Built-in accessibility: keyboard, focus, labels, contrast.
- Asleeping API: prefer
variant+sizeover dozens of booleans. - Styles only via system tokens; no loose hex in the primitive.
type ButtonProps = {
variant?: "primary" | "secondary" | "ghost" | "danger";
size?: "sm" | "md" | "lg";
loading?: boolean;
disabled?: boolean;
children: React.ReactNode;
};
Patterns
- Page headers, settings forms, empty states, confirm dialogues — document as a pattern if they repeat 3+ times.
- Patterns may have opinions on structural copy (“Title + description + CTA”), not on feature-specific copy.
- If a pattern serves only one screen, it isn't system yet — leave it in the app.
Governance
- Source of truth: UI package / folder
packages/ui(or equivalent) + Storybook. - Breaking change to token/component → semver versioning + changelog.
- Process: propose → document story → adopt in 1–2 screens → expand.
- Deprecate with a migration path; do not delete silently.
- Figma (if any): same token/component names; drift is a bug.
Product adoption
- Inventory one-offs (duplicated buttons/inputs/colors).
- Extract primitive + tokens.
- Replace call sites without changing UX unintentionally.
- Story + docs (
storybook-docs). - Forbid new literals via lint/review when the team is ready.
Apps must import from the design system — do not copy the Button to features/.
Accessibility and i18n
- Contrast and focus in tokens/components, not "later on the screen".
- Do not embed product strings in primitives; patterns may have overridable defaults.
- Support densities / zoom / RTL if the product requires it — plan it in the primitive.
Anti-patterns
- Design system = just a button library without tokens
- Semantic tokens mixed with
blue-500on screens - Infinite variants for a one-feature case
- Documentation only in Figma, divergent code
- Breaking APIs without major/changelog
- Restyling a primitive with fragile global CSS per page
Flow when evolving the system
- Which layer? (token vs primitive vs pattern)
- Is there something reusable? Extend before creating.
- Implement with tokens + a11y + light tests if the repo has them.
- Stories/docs with do/don't.
- Migrate critical call sites; announce deprecations.
Definition of done
- Semantic tokens covering the change
- Component/pattern in the right layer, without a disguised one-off
- Basic states and a11y covered
- Documented in Storybook (or monorepo docs)
- Consumer apps use the system without new loose hex/spacing
- Breaking changes versioned or avoided