Component Model, Design Systems & Styling — Cheat Sheet

§2.8 + §2.11 · Pair with lesson 0015-design-system-styling.html · Rapid-fire: “CSS-in-JS runtime cost vs zero-runtime”

1The frame

Design system = product, not folder: tokens (decisions) → components (behavior+a11y+look) → governance (versioning, docs, contribution model).

Why a Lead cares: 5 teams × 5 datepickers = bundle bloat (LCP/INP), N bad a11y implementations, brand drift. The system is a perf + velocity instrument.

Say: “Tokens are its data, components are its API, governance is its release process.”

2Component API patterns (§2.8 — know by name)

PatternOne-liner
Composition > inheritance/configProp explosion (Nth boolean) → open children/slots instead. React has no inheritance on purpose.
Controlled vs uncontrolledParent owns state (value/onChange) vs component owns it (defaultValue/ref). Libraries ship both, like the DOM <input>.
Compound componentsTabs.List/Tab/Panel share implicit state via context created in the parent. Cure for prop explosion. Trap: pieces break outside parent; portals can sever context.
Render props → hooksHooks replaced them for logic reuse. Render props survive where the component controls what/where/how-many renders (virtualized rows, table cells).
HeadlessBehavior+state+a11y, zero styles (Radix, React Aria, TanStack Table). You bring the look.

3The staff-level architecture

Headless core + token-driven styled layer per brand. Keyboard/focus/ARIA solved once centrally; new brand = new token set + thin wrappers, not a fork.

Say: “Headless for behavior, tokens for look — one library, many brands, no forked focus management.”
Trade-off to volunteer: headless pushes styling work to consumers. Single-brand startup → pre-styled lib (MUI) is faster. The split earns its cost at multi-team/multi-brand scale.

4Styling spectrum — ONE axis

“When is the CSS produced — build time or render time?”

ApproachCSS atVerdict
CSS Modules / Sassbuildzero runtime, cacheable .css
styled-components / Emotionrenderper-render tax, no RSC
vanilla-extract / Linaria / Panda / StyleXbuildCSS-in-TS DX, static output, RSC-safe
Tailwindbuildtokens-as-utilities, purged, RSC-safe; verbose markup

Mechanism (say it): runtime CSS-in-JS serializes + inserts rules during render, on the main thread, again on re-render; styles ship as JS not cacheable CSS.

Receipt: Emotion's #2 maintainer (Magura/Spot): 54ms → 27.7ms (≈48% faster) moving to Sass Modules.

Kill shot: runtime CSS-in-JS can't run in Server Components → everything becomes "use client", defeating RSC's zero-JS goal (Next.js docs).

Say: “Zero-runtime keeps the DX and moves the cost to build time. Dynamic styles compile down to CSS variables.”

5Design tokens — tier model

--blue-500: #5392f9;            /* 1 primitive (never use directly) */
--color-action-primary:
    var(--blue-500);            /* 2 semantic = THE decision */
--button-bg:
    var(--color-action-primary);/* 3 component (optional) */

Components consume semantic only → dark mode / new brand = re-map semantic layer, zero component edits.

Fresh fact: W3C DTCG shipped the first stable token format spec — 2025.10 (vendor-neutral JSON, aliases, color spaces) → standard interchange Figma ↔ Style Dictionary ↔ platforms.

Say: “Tokens name the decision, not the value.”

6Dark mode — production details

  • Default to system: @media (prefers-color-scheme: dark) on :root:not([data-theme]).
  • Explicit user choice wins: [data-theme="dark"] overrides.
  • No flash: saved choice is in storage → inline script in <head> sets data-theme before first paint. useEffect = guaranteed flash (runs after paint — L02 CRP).

7Modern CSS pair (design-system-shaped)

@layer: later layer beats earlier regardless of specificity; unlayered beats all layers. Ship library CSS in a named layer → consumers override without !important. Ends the specificity war structurally.

Container queries: container-type: inline-size + @container (min-width: …) — component adapts to its container, not viewport. Same card: wide in results, stacked in sidebar, same screen. THE design-system responsive primitive.

8“Shared library for 30 teams” — 5 beats

  • Architecture: headless core + token styled layer; build-time styling (SSR/RSC-safe).
  • Distribution: versioned npm pkg; tree-shakeable (ESM, sideEffects, per-component entries) — Button users don't ship DataGrid.
  • Change: semver; breaking = deprecation window + codemod, never a flag day (migration cost ×30).
  • Governance: InnerSource (teams PR in, core team curates); Storybook as contract; track adoption + duplication.
  • Guardrails: CI size budget on the package, a11y tests in core, visual regression on skins, @layer-wrapped CSS.
Say: “The design system is the one dependency I deliberately re-couple — even across MFEs (build-time / MF singleton). Two Buttons on one page is a UX bug, not autonomy.” (ties L14)

9Memorize

  • “A design system is a product: tokens, components, governance.”
  • “Styling has one axis: when is the CSS produced.”
  • “Runtime CSS-in-JS pays per render and can't enter Server Components.”
  • “Tokens name the decision, not the value.”
  • “Headless for behavior, tokens for look.”
  • “Unlayered beats layered — ship the library in a layer, the app always wins.”
  • “One Button. Autonomy everywhere else.”

10Don't fail the interview — traps

  • “CSS-in-JS is slow” with no mechanism. Name it: serialize + insert rules during render, on the main thread, styles shipped as JS.
  • Dunking on CSS-in-JS. It won for reasons (colocation, no collisions, variants). Frame: “kept the wins, moved the cost to build time.” Small CSR app → tax may be invisible.
  • Forgetting the RSC angle. Runtime CSS-in-JS ⇒ "use client" everywhere. This is the 2026 deciding argument.
  • Tokens = primitives. var(--blue-500) in a component defeats theming. Semantic layer or it didn't happen.
  • Theme in useEffect → flash of wrong theme. Inline head script, before paint.
  • “Hooks replaced render props” (full stop). Render props still own delegated rendering (virtualizers).
  • Controlled-only or uncontrolled-only APIs. Primitives ship both modes.
  • Breaking changes by decree. 30 teams = deprecate → codemod → remove.
  • Solving overrides with specificity. The structural answer is @layer.
Sources: react.dev · patterns.dev · Radix · Magura / Spot · InfoQ · Next.js CSS-in-JS · DTCG 2025.10 · MDN @layer · MDN container queries