You would never write if (user.plan === "pro" && daysActive > 14) with the 14 repeated in eleven other files. You'd pull it out into const TRIAL_LENGTH = 14, name it, and change it in one place when marketing wants a 30-day trial. You know magic numbers are a smell. You've refactored them out of other people's code and felt a little smug about it.
Then you open your own stylesheet and there it is: #3b82f6 typed into 47 components. padding: 13px here, padding: 16px two files over, padding: 0.8rem in a third because past-you was experimenting. A button whose blue is one shade off from the link blue because you eyeballed it at 11pm. This is the same crime you'd reject in a code review — hardcoded constants scattered across the codebase with no single source of truth — except you've given it a respectable-sounding name. You call it "the CSS."
Design without DRY is still a violation of DRY. The fix is something you already understand in your bones, just applied to a part of the codebase you've been treating as exempt from your own standards.
Open the CSS for any side project you shipped without a designer and grep for a hex code. Count the occurrences. Then grep for px. The numbers will horrify you, because they're the same numbers, slightly wrong, over and over.
Here's the typical shape of the problem:
/* button.css */
.btn-primary { background: #3b82f6; padding: 10px 16px; border-radius: 6px; }
/* link.css */
a { color: #3a82f7; } /* one digit off, by accident */
/* badge.css */
.badge { background: #3b82f6; padding: 4px 9px; border-radius: 5px; }
Three files. The "same" blue typed three times, already drifting. Padding and radius chosen by feel each time, so nothing lines up. Now imagine the rebrand request: make the primary blue a little more purple. In a sane codebase that's a one-line change. Here it's a find-and-replace across the whole repo, and you'll miss the #3a82f7 typo because it doesn't match the search string. You'll ship a button and a link in subtly different shades and never notice until a user screenshots it.
This is the failure mode the Decide Once, Reuse Forever principle warns about — you're re-deciding the same value every time you need it, and each decision is a fresh chance to be inconsistent. The repeated #3b82f6 is a constant that forgot it was a constant.
Design without DRY is still a violation of DRY. You've just given the magic numbers a respectable name and called it "the CSS."
The reason this happens is that CSS doesn't force you to name things the way a strict type system might. Nothing breaks if you hardcode. The page renders. The smell stays invisible until the day you need to change one decision in fifty places, and by then the damage is set in stylesheet.
A design token is a named constant for a design value: a color, a spacing step, a font size, a border radius, a shadow. That's the entire concept. If you've ever written const PRIMARY = "#3b82f6", you've already made a design token. You just didn't call it that, and you probably didn't use it everywhere you should have.
Design tokens are named constants for design values: color, spacing, type, radius, shadow. The reasoning is identical to why you never scatter magic numbers through business logic. Name the value once, reference the name everywhere, change it in exactly one place. A token is a variable; your design system is the file where those variables live.
Strip away the design-world vocabulary and the mental model maps onto things you do daily:
:root {
--color-primary: #3b82f6; /* like const PRIMARY */
--space-4: 16px; /* like const GAP_M */
--radius-md: 6px; /* like const RADIUS */
--text-lg: 1.125rem; /* like const SIZE_LG */
}
.btn-primary {
background: var(--color-primary);
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-md);
}
Now the blue lives in one declaration. The rebrand becomes a one-line edit. The typo is impossible, because you're never re-typing the value — you reference the name, and a misspelled var(--color-primry) fails loudly instead of rendering a slightly-wrong shade. That last part matters more than it looks: a wrong constant in CSS normally fails silently with a plausible result, which is the worst kind of bug. Tokens convert silent visual drift into a name you either spelled right or you didn't.
A design token is a constant. Everything you already believe about magic numbers in code applies, unchanged, to the hex codes and pixel values in your CSS.
This is where the rest of your design vocabulary comes home. The 8-Point Grid is a set of spacing tokens, each a multiple of 4 or 8. The Type Scale is a set of size tokens picked from a ratio, so you never type an arbitrary px size again. The 60-30-10 color split is a tiny palette of tokens with a clear role for each. Tokens are the mechanism; those chapters were the policies. You've been describing constants this whole book — this is the chapter where you declare them.
The single mistake that turns a good token system into a useless one is collapsing it into a single layer. You need two, and the distinction is the whole game.
Primitive tokens are the raw palette. They describe what the value is, with no opinion about where it's used. They are your full range of blues, your full spacing ramp, every font size you might reach for:
:root {
/* primitives: raw values, named by what they ARE */
--blue-50: #eff6ff;
--blue-500: #3b82f6;
--blue-600: #2563eb;
--blue-700: #1d4ed8;
--gray-50: #f9fafb;
--gray-500: #6b7280;
--gray-900: #111827;
}
Semantic tokens describe what the value is for. They never hold a raw hex or px — they point at a primitive. The name encodes intent, not appearance:
:root {
/* semantic: named by ROLE, pointing at primitives */
--color-primary: var(--blue-600);
--color-primary-hover: var(--blue-700);
--color-text: var(--gray-900);
--color-text-muted: var(--gray-500);
--color-surface: #ffffff;
--color-border: #e5e7eb;
--space-card: 24px;
}
Here's why the indirection earns its keep. Components only ever consume semantic tokens. A button references --color-primary, never --blue-600. The button does not know or care that primary happens to be blue today. This is the same reason you program against an interface, not a concrete class: you decouple the consumer from the implementation.
Components reference what a value is for, never what it is. A button asks for the primary color; it never needs to know that primary is blue.
The payoff arrives the moment something changes. Marketing wants the brand to go from blue to indigo? You repoint one semantic token — --color-primary: var(--indigo-600) — and every button, link, active tab, and focus ring updates at once, because they all asked for "primary," not "blue." If components referenced --blue-600 directly, you'd be back to find-and-replace, just with a fancier name on the constant.
The discipline that makes this work is one rule: a component may use var(--color-primary) but never var(--blue-600). The day a button references a primitive directly, the indirection is broken and your theming superpower quietly dies. Treat reaching past the semantic layer the way you'd treat a UI component importing from your database layer: a boundary violation, not a shortcut.
A useful gut check: a primitive token's name should make no sense as a sentence about purpose (--blue-600 tells you nothing about a button), while a semantic token's name should read like intent (--color-primary, --space-card, --color-danger). If you can't tell which layer a token belongs to from its name alone, the name is wrong.
You have two reasonable places to put tokens, and they aren't mutually exclusive. The non-negotiable part isn't which you pick — it's that there is exactly one source of truth and everything reads from it.
This works in any project, framework or none, and it's my default for plain CSS or when I want runtime theming. One file, imported once:
/* tokens.css: the single source of truth */
:root {
--blue-600: #2563eb;
--gray-900: #111827;
--space-6: 24px;
--radius-md: 6px;
--color-primary: var(--blue-600);
--color-text: var(--gray-900);
}
Every other stylesheet consumes var(--…) and never hardcodes. That's it. The browser resolves the values at runtime, which is what makes theming almost free in the next section.
If you're on Tailwind — which, as an indie hacker shipping fast, you very likely are — your tailwind.config.js is a token file. The theme object is where your constants live, and every utility class is a reference to one:
// tailwind.config.js: tokens as config
module.exports = {
theme: {
extend: {
colors: {
primary: { DEFAULT: "#2563eb", hover: "#1d4ed8" },
surface: "#ffffff",
},
spacing: { card: "24px" },
borderRadius: { md: "6px" },
},
},
};
Now bg-primary, p-card, and rounded-md are your semantic tokens, enforced by autocomplete. The win is subtle but real: a teammate (or future you) reaching for a one-off bg-[#3b82f6] arbitrary value sticks out in review like a raw SQL string in an ORM codebase. The config makes the named path the path of least resistance.
The strongest setup combines both: define primitives as CSS variables, then map Tailwind's theme onto those variables (for example, primary: "var(--color-primary)"). Now you get Tailwind's ergonomics and runtime theming. But don't reach for that on day one. Pick one layer, get every component consuming it, and add the bridge only when you actually need runtime swaps.
Tokens are easy; naming them is the work. Keep semantic names role-based and boring: --color-primary, --color-danger, --color-text-muted, --space-card. Resist --color-button-blue (couples role to value) and --color-brand-blue-2 (what is 2?). If you'd be embarrassed to read the name aloud in a design review, rename it now while you have three usages instead of three hundred.
Here's the part that converts skeptics. Once components read only from the semantic layer, you get dark mode and white-labeling almost for free — not as a rewrite, but as a remapping. You aren't restyling components. You're re-pointing constants.
Dark mode in a two-layer system is one extra block. The semantic tokens keep the same names; you feed them different primitives under a selector:
:root {
--color-surface: #ffffff;
--color-text: var(--gray-900);
--color-border: #e5e7eb;
}
[data-theme="dark"] {
--color-surface: var(--gray-900);
--color-text: var(--gray-50);
--color-border: #374151;
}
Toggle data-theme="dark" on the html element and the entire app flips. Not one component changed. The button still asks for --color-surface; the answer changed underneath it. This is the clearest argument for the semantic layer: it's the difference between a feature you ship in an afternoon and one you keep postponing because it would mean touching every component.
White-labeling is the same trick wearing a suit. A client wants the app in their brand color? You ship a per-tenant block that overrides the primary and a couple of accents, nothing more:
[data-tenant="acme"] {
--color-primary: #d4451f; /* Acme orange */
--color-primary-hover: #b8390f;
}
The deeper point: theming isn't a feature you bolt on later. It's a property that emerges when your tokens are structured right. Teams that "can't do dark mode" usually don't have a color problem — they have a missing semantic layer, and every color is baked into a component that has no idea what role it's playing.
If your components read only the semantic layer, dark mode and white-labeling stop being projects and become a single override block.
This is also the cleanest example of the Component Contract in practice. A well-built component promises "I render in whatever the primary color currently is." It accepts the token as part of its contract and makes no assumption about the literal value. Tokens are how that promise gets kept across themes you haven't even designed yet.
Teams that "can't do dark mode" rarely have a color problem. They have a missing semantic layer.
You don't need to tokenize the whole app in a day. Work the order below — each step is checkable, and you can stop after any of them with the codebase strictly better than you found it.
Search for hex codes and for px or rem across your stylesheets. Count the distinct-but-similar values. That count is your debt. Success looks like a list of the 8 to 15 raw values you actually use, with the accidental duplicates exposed.
Create one tokens.css (or fill in tailwind.config.js). Pull every real color into a named ramp, every spacing value onto an 8-point ramp, every font size onto your type scale. Name them for what they ARE. Success: a single file holds every raw value in the project.
On top of the primitives, define role-based tokens that point at them: --color-primary, --color-text, --color-text-muted, --color-surface, --color-border, --space-card. No raw values here, only references to primitives. Success: you can read the semantic block aloud and it describes your UI's roles.
Pick your button. Replace every hardcoded color, padding, and radius with semantic var(--…) references. Confirm it looks identical to before. Success: the button has zero literal hex or px values and renders unchanged.
Add a single [data-theme="dark"] block that re-points your semantic color tokens to darker primitives. Toggle the attribute on html. Success: your converted button flips to dark with no component edits, which proves the architecture holds.
If on Tailwind, treat an arbitrary value like bg-[#3b82f6] as a code smell in review. If on plain CSS, do a final grep to confirm no component still references a primitive directly. Success: the path of least resistance is the token, not the literal.
You already refuse to scatter magic numbers through your business logic. Your stylesheet was never an exception. It was just the one place you let the rule slide.
A hex code typed twice is a constant you forgot to name. Name it once, and your design starts to change in one place instead of fifty.