Part
1
  |  
Seeing Like a Designer
  |  
Chapter
4

Decide Once, Reuse Forever

Why a small system of constraints makes you faster and better than designing freely
Reading Time
13
mins
BACK TO DESIGN FOR DEVELOPERS

Here is the thing that feels like creativity and is actually the reason your app looks unfinished: you decide every value the moment you need it. A card needs padding, so you type 13px because it looks about right. A button needs a background, so you reach for #3a7 because green means go. A heading needs to be bigger, so you bump it to 17px. Each choice is reasonable in isolation. Each one is also a brand-new decision, made under no constraint, with a sample size of one.

That open field of choice feels like freedom. It is the opposite. Unconstrained per-element decisions are exactly what makes developer-built interfaces look noisy and feel slow to assemble. You are not too free; you are too unconstrained. The fix is not more taste applied harder. The fix is to stop deciding things one at a time.

The trap: infinite choice is a tax

You already know this instinct from code. You would never let every function pick its own indentation, its own quote style, its own naming convention. You install Prettier and an ESLint config so those decisions get made once and then disappear. Nobody on a good team argues about tabs at 3pm. The decision is settled, encoded, and forgotten.

Then you open a stylesheet and throw all of that discipline away. Every margin is a fresh judgment call. Every gray is hand-mixed. Every font size is whatever felt right in the half-second before you typed it.

The cost of this is not mainly the time you spend deciding, though that is real. The deeper cost is that a value chosen freely almost never matches another value chosen freely. Your padding ends up being 12, 13, 16, 14, and 20 across five components that should look like siblings. Your grays drift from #888 to #8a8a8a to #909090. None of these gaps is visible on its own. Together they read, unmistakably, as unfinished — the same way inconsistent indentation reads as junior even when the logic is fine.

You are not too free; you are too unconstrained.

This is why "just eyeball each value" never converges. Eyeballing has no memory. The you who picked 13px on Tuesday is a different decision-maker from the you who picks 15px on Thursday, and neither of you wrote anything down. There is no force pulling the values toward each other, so they spread. Consistency is not something you can reach by trying harder at each individual choice. It has to be a property of the system, not an outcome of your discipline in the moment.

When you blur your app with the Squint Test and the spacing looks lumpy and the type looks like it is at six slightly-different sizes, this is the cause. Not bad taste. Too many independent decisions.

Constraints make you faster and better

Here is the paradox that takes developers a while to believe: giving yourself fewer options produces work that is both more coherent and quicker to build. We are trained to think freedom and quality move together. In interface work they pull apart.

You already trust this everywhere else in your stack.

  • An enum is strictly less expressive than a free-text string. That is the entire point. status: "shipped" cannot be misspelled as "shippd", cannot drift to "Shipped" in one row and "shipped" in another, cannot accumulate seventeen synonyms for the same state. The constraint is what makes the data clean.
  • A linter removes your freedom to format code however you like, and in exchange every file in the repo looks like one person wrote it.
  • A type signature is a contract that rejects most of the values you could pass. That rejection is the feature.
  • Design by contract narrows what a function will accept so the rest of the system can trust it.

Design is the same discipline applied to pixels. When the only padding values that exist in your app are 4, 8, 12, 16, 24, 32, and 48, you cannot produce the lumpy 12-13-14-16-20 mess. It is not available to you. You picked from a set, and the set was internally consistent, so the result is consistent by construction.

Framework · Decide Once, Reuse Forever · DORF

Stop making per-element design decisions. Decide each kind of thing one time — a set of font sizes, a set of spacing values, a small set of colors, one corner-radius scale — and from then on only ever pick from those sets. Design stops being invention from a blank page and becomes selection from a system you already own.

The speed gain is the part people miss. When you are choosing padding for a new card, the open question "what number looks right here?" has infinite answers and no way to be confidently wrong. The constrained question "which of these seven values fits?" has seven answers, and you can usually rule out five at a glance. You are choosing between 16px and 24px, not between 16px and the entire number line. Decisions that used to cost thirty seconds of fiddling cost two seconds of picking.

Key takeaway

Fewer options is not a limitation on your design; it is the mechanism that makes the design consistent and fast.

There is a second-order benefit. Once the small decisions are settled, your attention is free for the decisions that actually require judgment — what goes on the screen, what the user is trying to do, what the one loudest thing should be. You only have so much design attention per session. Spending it on "is this 14px or 15px" is a waste of a scarce resource. Spend it on the questions a system cannot answer for you.

Constraints, not a cage

A constrained set is a default, not a prison. Real designers occasionally break the grid for a deliberate reason. But they break a system that exists — the exception is visible because everything else is regular. Breaking a rule you never had is just inconsistency wearing a confident face.

The four decisions to make once

So which decisions do you settle up front? Four sets carry almost all of the visual weight in a typical SaaS interface. Make these once and most of the noise disappears. This is the roadmap for Part II — each gets its own chapter — so treat what follows as a map, not the full route.

1. A type scale — a fixed set of font sizes. Not a size per element. A short ladder of sizes, usually five or six, derived from a single ratio so the jumps feel related instead of random. Body text, small text, and three or four heading sizes is enough for most apps. Once the ladder exists, a heading is never 17px; it is "the third rung." Chapter 5 builds this as The Type Scale — once you have it, you never type an arbitrary pixel size again.

2. A spacing scale — a fixed set of gaps and paddings. Every margin, padding, and gap comes from one short list of multiples, almost always built on 8 (with 4 available for tight cases): 4, 8, 12, 16, 24, 32, 48, 64. No 13px. No 21px. Chapter 6 makes this The 8-Point Grid, where consistency stops being something you check and becomes something the math guarantees.

3. A color set — a small, named palette. Not a color per component. A neutral ramp for backgrounds, borders, and text; one or two brand colors; a small number of semantic colors for success, warning, and danger. That is the whole box of crayons, and you only ever draw with what is in the box. Chapter 7 governs how to spend those colors with 60-30-10: mostly neutral, some secondary, a little accent.

4. A radius and shadow set — one corner scale, a couple of elevations. One small set of corner radii (say 0, 6px, 12px, and full for pills) and two or three shadow definitions for "raised" and "floating." Pick them once. A modal and a dropdown should not have subtly different corners and shadows because you wrote them on different days.

A heading is never seventeen pixels. It is the third rung of a ladder you already built.

Notice what these four have in common. Each replaces a per-element question with a pick-from-a-set question. Each is small — single digits of options, not dozens. And each is the kind of thing you can decide in an afternoon and then ride for the life of the product. The rest of this book mostly assumes these four sets exist; this chapter is the argument for why you build them at all.

You do not need to perfect the contents of these sets today. A merely decent scale that you actually apply everywhere beats a perfect scale you apply to half the app. Consistency is doing more work here than optimality. A coherent app built from slightly-wrong constants looks far more finished than an incoherent app built from individually-tuned ones.

Encode the system so reuse is automatic

A system that lives in your head is not a system. It is a good intention that decays the moment you are tired or in a hurry. The whole point of DORF is that the decision is made, which means it has to live somewhere the code reads, not somewhere you have to remember.

For a developer this is the easy part, because the tool already exists and you already use it for other constants. Put the sets in CSS custom properties:

:root {
  /* spacing scale */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;

  /* type scale */
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-lg: 1.25rem;
  --text-xl: 1.75rem;

  /* color set */
  --bg: #ffffff;
  --surface: #f7f7f8;
  --border: #e4e4e7;
  --text: #18181b;
  --muted: #71717a;
  --accent: #4f46e5;

  /* radius set */
  --radius-sm: 6px;
  --radius-md: 12px;
}

Now a component does not contain decisions. It contains references to decisions.

.card {
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
}

Read that .card rule again. There is not a single raw value in it that a future you could second-guess. There is no 13px waiting to drift to 14px. Every property points at a settled decision. If --space-4 is right for one card, it is right for all of them, automatically, forever — that is the "reuse forever" half of the name doing its work.

If you are on Tailwind, you already have a version of this, and the move is to make the theme the single source of truth instead of routing around it. Stop reaching for p-[13px] and text-[17px] arbitrary values. Define the sets in the theme and use the named utilities:

// tailwind.config.js
export default {
  theme: {
    extend: {
      spacing: { 18: "4.5rem" },
      colors: {
        surface: "#f7f7f8",
        accent: "#4f46e5",
      },
      borderRadius: { card: "12px" },
    },
  },
};
// the decision is in the theme; the component just picks
<div className="p-4 rounded-card border border-zinc-200 bg-surface">
  ...
</div>
Arbitrary values are a smell

Every time you write a Tailwind arbitrary value like mt-[13px] or text-[#3a7], you are stepping outside the system to make a one-off decision — the exact habit DORF exists to kill. A few are fine. A codebase full of them means you have a config file you are ignoring. Grep for \[ in your className strings; the count will tell you the truth.

These variables are not just convenient. They are named constants for design, and they earn DRY for your visual layer the same way constants earn it for your logic. That idea is large enough to get its own treatment later as Tokens as Constants — the discipline of treating every design value as a named token, never a literal — and it is what lets you do things like ship a dark mode by reassigning a dozen variables instead of hunting through every component. For now, the point is narrower and load-bearing: the system only saves you if the code reads it instead of you remembering it.

✕ Per-element decisions
  • Padding: 12, 13, 16, 14, 20 across sibling cards
  • Grays mixed by hand: #888, #8a8a8a, #909090
  • Every new value is a fresh, lonely judgment call
  • Dark mode means editing hundreds of literals
✓ Decide Once, Reuse Forever
  • Padding is always var(--space-N) from one list
  • One neutral ramp, referenced everywhere
  • New value = pick from a set in two seconds
  • Dark mode means reassigning a dozen variables

What to do Monday morning

Audit what you actually have

Open your main app surface and write down every distinct font size, every distinct spacing value (margin, padding, gap), and every distinct color in use. Grep the codebase if you can. The list is almost always two to three times longer than you expected — that shock is the point, and it is the whole motivation for the rest.

Collapse each list into a small set

Take your sprawling list and force it down. Font sizes to five or six. Spacing values to a single 8-based ladder (4, 8, 12, 16, 24, 32, 48). Colors to a neutral ramp plus one or two brand colors plus three semantic ones. For each old value, decide which surviving value it maps to.

Encode the sets as variables

Put the four sets into :root as CSS custom properties, or into your Tailwind theme. This is the moment the decisions stop living in your head and start living where the code can read them. Give each one a clear name.

Swap one component over completely

Pick a single component — a card, a button, a form field — and replace every raw value in it with a var(--…) or a themed utility. Do not move on until that one component has zero hardcoded design literals. This proves the system works before you commit to it everywhere.

Add a tripwire for the arbitrary stuff

Grep your project for hardcoded px values and Tailwind arbitrary brackets ([). Keep that command handy. Each match is either a value that belongs in a set or a deliberate, documented exception — and you should know which.

Stop designing each element. Design the set of choices once, then spend the rest of your life just picking from it.