How would you design a reusable component library across teams?
Token-driven theming, headless primitives + styled wrappers, semantic-versioned releases, automated visual regression, and a contribution model that prevents fragmentation.
A component library that serves N product teams is a product in its own right, with its own roadmap, SLOs, and consumers. The dominant failure mode is fragmentation: a team forks because the library lacks one prop they need, that fork copy-pastes into a second team, and within a year you have three Button components and no consistent look. The architecture and the operating model both have to prevent that.
1. Tokens first, components second. Before any component is built, define the token layer: color (raw → semantic → component-specific), spacing scale, typography ramp, radii, shadows, motion durations, easing, z-index. Express tokens as CSS variables (--color-bg-surface) so theme switching (light/dark/brand) and white-labeling are free. Components consume semantic tokens (var(--color-text-primary)), never raw values. Tools: Style Dictionary, Tokens Studio, or a flat JSON exported to multiple targets.
2. Headless + styled split. Behavior (focus management, ARIA, keyboard, RTL handling) goes into headless primitives — adopt Radix UI, Ark UI, Headless UI, or React Aria rather than rebuilding. On top, your library owns a thin styled layer that applies tokens. This means: a consumer who needs a weird visual variant doesn't have to fork — they drop down to the headless layer. shadcn/ui's "copy into your repo" model is the most extreme version of this: every consumer literally owns the component, no version pinning, full local customization. Pick the model that fits your governance.
3. API discipline. A library lives or dies by its public API. Rules of thumb:
- Composable, not monolithic.
<Card><Card.Header/>...</Card>lets consumers omit pieces; a 12-prop<Card title body footer leftIcon ...>traps everyone. - Polymorphic via
asprop or Radix'sasChildso consumers can render the right semantic element (<Button as="a">for a link styled as a button). - Controlled + uncontrolled variants with consistent prop names (
value/defaultValue,open/defaultOpen). - No leaky internals. Never expose internal CSS class names, internal context, or component-internal data attributes. They become de facto APIs and break refactors.
- Forward refs, support
classNameandstyleoverrides on the outermost element, but only as escape hatches.
4. Versioning & deprecation. Strict SemVer. Major versions cluster breaking changes; minors only add; patches only fix. Pair every deprecation with: a @deprecated JSDoc tag (IDE hint), an ESLint rule that warns on usage, a codemod that auto-migrates, and a removal target version. Use changesets for per-PR changelog entries. Never break a public API in a minor — consumers will lose trust and freeze versions.
5. Quality bar (CI):
- Visual regression: Storybook + Chromatic (or Playwright + percy / Loki). Every component renders every variant; PRs surface visual diffs.
- Accessibility:
axe-coreagainst every story; deny merge on regressions. - Type safety: TypeScript strict, no
anyin public types,tsdtype tests for tricky generics. - Bundle budget: per-component size budget enforced with size-limit; PR fails if a button balloons from 1KB to 10KB.
- Unit + interaction tests:
@testing-library+ Playwright Component Testing.
6. Distribution. Publish to a private npm registry per major version (e.g., @acme/ui@2 and @acme/ui@3 side-by-side during a migration window). For monorepos consuming the lib, ship via Turborepo / Nx so consumers pick up changes via build graph. Avoid CSS-in-JS that ties consumers to a specific runtime; prefer CSS modules, Vanilla Extract, or Tailwind with token preset.
7. Documentation. Storybook is the canonical doc — runnable examples, prop tables, accessibility notes. Pair with a written usage guide ("when to use Modal vs Sheet"). Documentation is part of the API; ship docs in the same PR as the component.
8. Contribution & governance model. This is where most libraries die. Decide upfront: federated (any team can contribute), centralized (a platform team owns it), or hybrid (federated for components, central for tokens & primitives). Require an RFC for new components with proof of demand ("3 teams need it"). Open office hours; a #design-system channel; a public roadmap. Communication is the real cost — engineering is the cheap part.
The product mindset matters most: the library has users, not consumers. Track adoption, measure component coverage per app, do user research on what's painful, and prune unused components after a year. A library nobody can keep up with is worse than no library.
Code
Follow-up questions
- •How do you prevent teams from forking a component when their use case isn't supported?
- •What's your visual regression strategy?
- •How do you handle theming for white-label apps?
Common mistakes
- •Designing components in isolation from real product use cases.
- •Exposing CSS classes as part of the public API.
- •No SemVer discipline — every change feels major.
Performance considerations
- •Tree-shake-friendly exports (named, no re-export barrels with side effects).
- •Avoid heavy runtime-only theming — CSS variables compile away.
Edge cases
- •Form components with controlled+uncontrolled modes silently break unless asserted at the type level.
Real-world examples
- •Shopify Polaris, Atlassian Design System, GitHub Primer, shadcn/ui (un-library).