How do you build one component library that serves many teams? Three layers: component APIs that don't rot (composition, controlled/uncontrolled, compound, headless), a styling strategy chosen on runtime cost (CSS Modules vs CSS-in-JS vs zero-runtime vs Tailwind), and design tokens as the contract that makes theming and dark mode a data change, not a rewrite.
A component library is a bag of React components. A design system is a product with three layers and a release process:
Why a Lead cares: every team that rebuilds a datepicker ships another datepicker to users (bundle bloat → LCP/INP, L05/L08 budgets), re-solves accessibility badly, and drifts from brand. The design system is the deduplication and governance instrument — it's as much a perf and velocity play as a visual one.
“A design system is a product, not a folder of components. Tokens are its data, components are its API, governance is its release process.”
Shared components die one of two deaths: the prop explosion (40 booleans nobody dares remove) or the fork (teams copy-paste because the API can't bend). These five patterns are the vocabulary for avoiding both. The panel asks them by name.
React has no component inheritance story on purpose — you compose: pass JSX as children or as named “slot” props instead of subclassing or adding yet another flag. [react.dev] The smell to name: a component sprouting its Nth boolean (withIcon, iconLeft, iconIsSpinner…). Each flag doubles the state space and every consumer pays for every other consumer's feature. The fix is to open slots:
// configuration: the API absorbs every use case as a prop ❌
<Card title="…" subtitle="…" showBadge badgeColor="red" footerButtons={[…]} />
// composition: the API is a layout; content is the consumer's problem ✅
<Card>
<Card.Header><Badge tone="danger">Last room!</Badge></Card.Header>
<Card.Body>…</Card.Body>
</Card>
A component is controlled when its parent owns the state and drives it via props (value + onChange — the component is a pure function of props); uncontrolled when it owns its own state internally (defaultValue, or the DOM itself via a ref). [react.dev] Controlled = maximal flexibility (parent can sync it to the URL, validate, coordinate siblings) but more wiring; uncontrolled = zero ceremony but the parent can't reach in. The library answer: ship both — defaultValue for the easy path, value/onChange to take control — which is exactly what good primitives (and the DOM <input> itself) do.
Tabs / Tabs.List / Tabs.Tab / Tabs.Panel — a family of components that share implicit state through a React context created inside the parent, so consumers arrange the pieces in JSX instead of feeding a config object. [patterns.dev] It's the structural cure for prop explosion (2.1) and the native shape of every serious primitives library (Radix's Select.Trigger/Select.Content…). Trade-off: pieces are coupled to their parent's context — render a Tabs.Tab outside Tabs and it breaks, and portalling children can sever the link if you're careless.
Render props — passing a function that the component calls with its internal state (<Mouse render={pos => …} />) — were how we shared stateful logic before 2019. [patterns.dev] Custom hooks replaced them for logic reuse: same sharing, no wrapper pyramid, composable (useMouse()). [react.dev] The senior nuance: render props still earn their keep when the component needs to control what renders and where — e.g. a virtualizer handing you item state per row (<List>{item => <Row …/>}</List>). Hooks share logic; render props delegate rendering.
A headless component ships behavior, state, and accessibility with no styles: focus management, keyboard navigation, ARIA wiring, portal/overlay logic — you bring 100% of the look. Radix Primitives, React Aria, Headless UI, TanStack Table. [Radix] [React Aria] Why this is the design-system architecture: the hard, expensive, lawsuit-shaped part of a Select is the behavior+a11y, and it's brand-independent. So:
Build or adopt once (Radix/React Aria). Keyboard, focus, ARIA, RTL — solved centrally, tested centrally.
Each brand/team wraps the core with its tokens. New brand = new token set + thin wrappers — not a fork of the behavior.
“Headless for behavior, tokens for look. That split is how one library serves many brands without forking the hard parts — keyboard, focus, ARIA.”
The rapid-fire bank asks: “compare CSS-in-JS runtime cost vs zero-runtime.” The whole landscape becomes one question: is your CSS produced at build time or at render time?
| Approach | CSS produced… | Wins | Costs |
|---|---|---|---|
| CSS Modules / Sass *.module.css | build time | Plain CSS, scoped class names, zero runtime, cacheable .css file | Dynamic styles via inline style/CSS vars only; styles live in a separate file |
| Runtime CSS-in-JS styled-components, Emotion | render time | Colocation, full JS power, props→styles ergonomics | Runtime tax every render; SSR extraction complexity; doesn't work in Server Components |
| Zero-runtime CSS-in-JS vanilla-extract, Linaria, Panda, StyleX | build time (extracted from JS/TS) | Colocation + type-safety and static .css output; RSC-safe | Dynamic styling restricted to CSS-variable plumbing; build-tool integration |
| Utility-first Tailwind | build time (scanned) | Tokens enforced as utilities; tiny purged CSS; no naming; RSC-safe | Verbose markup; duplication across markup; needs component layer for reuse |
Runtime libraries serialize your style objects and insert CSS rules into the document while your components render — work on the main thread, repeated as components re-render, forcing the engine to re-evaluate styles against the DOM. Sam Magura — the #2 Emotion maintainer — measured a member-browser component at 54 ms with Emotion vs 27.7 ms with Sass Modules (≈48% faster) and moved Spot off runtime CSS-in-JS entirely. [Magura] [InfoQ] It also bloats the JS bundle (the library ships in your bundle; styles ship as JS, not as a cacheable .css file) and complicates SSR (style extraction, hydration mismatches).
The modern kill shot: React Server Components. Runtime CSS-in-JS needs to run in the browser and use state/context, so it simply doesn't work in Server Components — Next.js docs tell you to mark every styled component "use client" or use build-time approaches. [Next.js] If your rendering strategy is streaming SSR + RSC (L09), a render-time styling runtime is fighting your architecture. Zero-runtime libraries keep the DX (colocation, TypeScript) but extract real .css at build time — the trade is that fully dynamic styles must compile down to CSS custom properties.
“Runtime CSS-in-JS pays a tax on every render and can't run in Server Components. Zero-runtime keeps the DX and moves the cost to build time — same ergonomics, static CSS out.”
A design token is a named design decision (color.action.primary = #5392f9) stored as data, so the same decision can be compiled to CSS variables, iOS, Android, email. The W3C Design Tokens Community Group shipped the first stable token format spec (2025.10) — a vendor-neutral JSON format with aliasing/inheritance — so tokens now have a standard interchange format between Figma and code-side tools like Style Dictionary. [DTCG 2025.10] [Style Dictionary]
/* 1. primitive (raw palette — no meaning, never used directly) */
--blue-500: #5392f9;
/* 2. semantic (the decision — what it's FOR) */
--color-action-primary: var(--blue-500);
--color-surface: var(--gray-0);
/* 3. component (optional, scoped) */
--button-bg: var(--color-action-primary);
Components consume semantic tokens only. Then theming — dark mode, a new brand, a market variant — is re-mapping the semantic layer, touching zero component code:
:root { --color-surface: #ffffff; --color-ink: #16181d; }
/* system preference… */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) { --color-surface: #0f1115; --color-ink: #e7e9ee; }
}
/* …overridable by explicit user choice */
[data-theme="dark"] { --color-surface: #0f1115; --color-ink: #e7e9ee; }
Two production details that mark seniority: respect the system preference by default (prefers-color-scheme [MDN]) but let an explicit choice win; and kill the flash of wrong theme — the user's saved choice lives in storage, so a tiny inline script in <head> must set data-theme before first paint (an async-loaded effect runs after paint = flash; this is your CRP knowledge from L02 paying off).
“Tokens name the decision, not the value. Components only touch the semantic layer — so dark mode and new brands are a data change, not a refactor.”
Two modern CSS features the panel may probe because they're design-system-shaped:
@layerDeclare priority order up front: @layer reset, tokens, components, app;. A later layer beats an earlier one regardless of selector specificity, and unlayered styles beat all layers — so the app overrides the design system without !important or selector arms races. [MDN] Ship your library's CSS inside a named layer; consumers always win by default.
@container (min-width: 400px) styles a component by the size of its parent container (container-type: inline-size). [MDN] A media query can't make a HotelCard adapt when it sits in a narrow sidebar on a wide screen — a container query can. That's why it's the design-system responsive primitive: the component carries its own breakpoints anywhere it lands.
This is the actual §2.8 question. Structure the answer as architecture → distribution → governance:
singleton) — it's the one dependency you deliberately re-couple, because two button versions on one page is a UX bug, not autonomy. Make it tree-shakeable: ESM, sideEffects flagged, per-component entry points — teams using the Button shouldn't ship the DataGrid (L05/L12).@layer-wrapped CSS so consumers can always override (§5).“The design system is the one dependency I deliberately re-couple across teams — versioned, tree-shakeable, with codemods for breaking changes. Autonomy everywhere else; one Button.”
1. The shared Modal has grown to 23 props (showCloseButton, footerAlign, headerIcon…) and teams keep PR-ing more. The design-system fix?
2. Your library ships a SearchInput. One team needs to sync it to the URL (nuqs) and validate on every keystroke; most teams just want it to work with no wiring. What API do you ship?
3. How do Tabs.Tab and Tabs.Panel know which tab is active without the consumer passing state to each one?
4. The platform runs multiple brands; each needs its own look, but every dropdown must have correct keyboard navigation, focus trapping, and ARIA. The architecture?
5. Rapid-fire: why exactly is runtime CSS-in-JS (Emotion/styled-components) slower than CSS Modules? Name the mechanism.
6. You're moving search pages to RSC + streaming SSR (your L09 answer). The codebase is styled-components. What's the real constraint?
7. A teammate writes background: var(--blue-500) inside the BookingButton. It renders pixel-perfect. What do you flag in review?
8. Users with dark mode saved in localStorage see a white flash on every page load before the page turns dark. The fix?
9. Product teams complain they need !important to override your design-system CSS. The structural fix on the library side?
10. The HotelCard must show a horizontal layout in the main results column but stack vertically when it lands in the narrow map sidebar — on the same viewport. The right tool?
11. You must rename a prop across the design system — a breaking change consumed by 30 teams. The Lead rollout?
12. Rapid-fire: when would you still reach for a render prop in 2026, given hooks exist?
Answered: 0 / 12 · Correct first try: 0