A14 · Part 3 · Advanced
Design systems
Constraints that make the common thing easy and the inconsistent thing hard.
08On this page
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."
Opens in the editor: write it, run it, and check it against real tests.
All 54 for this level →Finished reading?
0 of 57 chapters read in this series