Skip to the chapter
←ReactGGroundwork⚛React
React/Advanced/A14
47 / 57 written chapters
/

The cover
  1. B1Setup & JSX
  2. B2Thinking in React
  3. B3Components
  4. B4Props
  5. B5useState
  6. B6Events & conditionals
  7. B7Lists & keys
  8. B8Forms
  9. B9useEffect
  10. B10Lifting state & styling
  11. B11Guided project
  12. ★Cheat page
  1. I1useRef
  2. I2Context
  3. I3useReducer
  4. I4Custom hooks
  5. I5Effects in depth
  6. I6Routing
  7. I7State architecture
  8. I8Memoisation
  9. I9Boundaries & portals
  10. I10Data fetching
  11. I11Forms at scale
  12. I12State libraries
  13. I13Auth
  14. I14TanStack Query
  15. I15Testing
  16. I16TypeScript
  17. I17Animation
  18. I18E2E testing
  19. I19i18n
  20. I20File uploads
  21. I21Real-time UI
  22. I22shadcn/ui
  23. ★Cheat page
  1. A1Fiber
  2. A2Concurrent
  3. A3Suspense
  4. A4Server Components
  5. A5Server Actions
  6. A6use()
  7. A7Rendering strategies
  8. A8Effect timing
  9. A9External stores
  10. A10Refs in depth
  11. A11Component patterns
  12. A12Performance
  13. A13Accessibility
  14. A14Design systems
  15. A15Advanced TypeScript
  16. A16Architecture at scale
  17. A17Custom renderers
  18. A18Migrating & upgrading
  19. A19Production debugging
  20. A20Review & judgement
  21. A21State machines
  22. A22Feature flags
  23. A23Interview bank
  24. ★Cheat page

⚛A14 · Part 3 · Advanced

Design systems

Constraints that make the common thing easy and the inconsistent thing hard.

3 min read8 sections2 exercises
47 of 57

In this chapter

  • Tokens are the foundation
  • Variants, typed
  • The escape hatch problem
  • Composition over configuration, again
  • Build on a headless base

and 3 more

about 3 min
08On this page
  1. Tokens are the foundation
  2. Variants, typed
  3. The escape hatch problem
  4. Composition over configuration, again
  5. Build on a headless base
  6. Making it usable by other people
  7. Server Components change the rules slightly
  8. Knowing when not to build one

Tokens are the foundation

:root {
  --space-1: 4px;  --space-2: 8px;  --space-3: 16px;
  --color-fg: #1f3a73;
  --color-bg: #fffdf6;
  --radius-md: 12px;
}
@media (prefers-color-scheme: dark) {
  :root { --color-fg: #d9e5fb; --color-bg: #191d25; }
}

A finite set of choices. When spacing is a scale rather than a number, nobody writes margin: 13px, and a theme change is a handful of redefinitions rather than a search across the codebase.

CSS custom properties cascade and can be swapped at runtime, which is why they beat JavaScript theme objects for this — no re-render, and they work in Server Components where there is no client theme provider.

Variants, typed

const button = cva("btn", {
  variants: {
    variant: { primary: "btn--primary", ghost: "btn--ghost" },
    size: { sm: "btn--sm", md: "btn--md" },
  },
  defaultVariants: { variant: "primary", size: "md" },
});

type ButtonProps = React.ComponentProps<"button"> & VariantProps<typeof button>;

function Button({ variant, size, className, ...rest }: ButtonProps) {
  return <button className={button({ variant, size, className })} {...rest} />;
}

The variants become a union type, so variant="primry" is a compile error rather than a silently unstyled button. And extending ComponentProps<"button"> means every real button attribute is typed without listing one.

The escape hatch problem

A system with no escape hatch gets forked

Someone will need a button with 2px more padding for one screen. If the system forbids it, they copy the component and change it — and now there are two buttons, one of which nobody maintains. Accept className and merge it. Make the common path easy, not the unusual one impossible.

function Button({ className, ...rest }) {
  return <button className={twMerge("btn btn--primary", className)} {...rest} />;
}

A merge utility matters because naive concatenation leaves both classes and the loser depends on stylesheet order. twMerge resolves conflicts so the caller's class actually wins.

Composition over configuration, again

<Card
  title="Notes" showFooter footerAlign="right"
  icon={<Star />} iconPosition="left" compact bordered
/>                                        // ✗ every new design adds a prop

<Card>
  <Card.Header><Star /> Notes</Card.Header>
  <Card.Body>...</Card.Body>
  <Card.Footer align="right">...</Card.Footer>
</Card>                                   // ✓ arrangement is the caller's

Boolean props that turn parts on and off are a sign the component should take children. Compound components are how design systems stay flexible without a prop for every layout anybody ever wants.

Build on a headless base

The hard parts of a select, a dialog, a tooltip or a date picker are roving focus, typeahead, collision detection, Escape handling and aria wiring. That is months of work and it is not where a design system adds value.

Radix, React Aria or Headless UI give you all of it unstyled. Your system becomes tokens, variants and composition on top — which is the part that is actually yours.

Making it usable by other people

  • Every component in one file per component, named after the file.
  • A live playground — Storybook or equivalent. A component nobody can see is a component nobody uses.
  • Document the props that matter, and the ones that do not exist on purpose.
  • Version it, and treat a prop rename as a breaking change. Consumers cannot fix what they cannot see coming.
  • Test behaviour, not markup. Snapshot tests of a design system are a permanent tax on changing anything.

Server Components change the rules slightly

Runtime CSS-in-JS does not work in a Server Component — there is no client to inject styles from. Systems built on styled-components are moving to CSS Modules, Tailwind, or zero-runtime libraries that extract styles at build time. If you are choosing today, pick something that works in both worlds.

Knowing when not to build one

A design system is a product with users, and its users are your colleagues. It needs documentation, a release process, and someone to answer questions. On a team of three with one application, a folder of shared components and a token file is the right size — and calling it a design system is what makes it expensive.

Interview answer, one sentence

"Tokens first, because a finite scale is what makes consistency the default; variants typed so a wrong one is a compile error; composition rather than boolean props so new layouts do not need new props; and always an escape hatch, because a system that cannot be adjusted gets copied instead."

02Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 5 testsOne component, rendered as whatever tag the caller needsadvanced
  • 7 testsA compound component, sharing state through contextadvanced
All 54 for this level →
A14

Finished reading?

0 of 57 chapters read in this series

→Next stop

  1. A13Previous · A13Accessibility
  2. A14you are here
  3. Next · A15Advanced TypeScript with ReactMaking the wrong call fail to compile, rather than fail in review.5 min read

01On this page

8 sections · 0%

  1. 01Tokens are the foundation
  2. 02Variants, typed
  3. 03The escape hatch problem
  4. 04Composition over configuration, again
  5. 05Build on a headless base
  6. 06Making it usable by other people
  7. 07Server Components change the rules slightly
  8. 08Knowing when not to build one

[] chapters · t top