Most developers think their app looks cheap because of the colors. They reach for a new palette, a trendy gradient, a darker dark mode, and the thing still looks like a side project. The colors were never the problem. It's the type — and unlike taste, most of it is fixable mechanically, no eye required.
Here's the part that should cheer you up: typography is the most rule-driven, least subjective surface in all of design. You don't need to feel anything. You need a scale, a line length, two font weights, and the discipline to stop typing arbitrary numbers. That's an afternoon of work, and it will do more for "looks professional" than any color change you could make. Type is the loudest amateur signal in software, and it's the easiest one to silence.
Open any product that looks unfinished and squint at the text — this is the Squint Test from chapter 3, pointed at one specific thing. You'll almost always find the same fingerprints: a heading that's barely bigger than the body, two paragraphs set at slightly different sizes for no reason, lines of text that run the full width of a 1440px monitor, and line-height left at the browser default of roughly 1.2 because nobody touched it.
None of those are color problems. They're spacing and proportion problems wearing a typeface. And they read as "amateur" instantly, even to users who could never name why. This is the Competence Tax from chapter 1 in its purest form: the reader can't see your database schema, so they judge your engineering by whether your h2 looks deliberate.
The encouraging flip side is that every one of those fingerprints has a numeric fix. Heading too small? It's not big enough relative to the body — that's a ratio. Lines too long? Cap the measure at a character count. Text feels cramped? Raise the line-height. There is no taste in any of this. There is arithmetic.
Typography is the most rule-driven, least subjective surface in design — you don't need an eye, you need a scale.
We start where the payoff is highest and the judgment required is lowest: deciding, once, what sizes your app is allowed to use.
Here's the habit that separates interfaces that look designed from interfaces that look assembled: designed ones use a type scale. A type scale is a small, fixed set of font sizes — usually five or six — where each step is the one below it multiplied by a single ratio. You pick the ratio once. Every size in your app comes from that set. You never type an arbitrary pixel value again.
Choose a small, fixed set of font sizes derived from a single ratio — 1.125, 1.2, or 1.25 — and only ever use sizes from that set. Pick the ratio once, generate five or six steps, encode them as named constants, and stop typing arbitrary pixel sizes forever. Consistency stops being a thing you maintain and becomes a thing the system guarantees.
This is the Decide Once, Reuse Forever pattern from chapter 4 applied to font size specifically. Instead of deciding 15px here and 17px there and 22px for that heading because it "looked about right," you decide the system once and let it answer every future question for you.
The ratio is the whole game. A small ratio like 1.125 (the "major second") gives gentle, closely-spaced sizes — good for dense interfaces like dashboards and admin panels where you don't want headings shouting. A larger ratio like 1.25 (the "major third") gives dramatic jumps — good for marketing pages and landing pages where you want a big confident hero. For most SaaS app UIs, 1.2 is a safe, boring, correct default. Boring is what you want here.
Start from a base of 16px — the body size, and the size you should almost never go below for primary text — and multiply up. With a ratio of 1.2 you get a clean ladder:
:root {
--text-xs: 0.75rem; /* 12px — captions, meta */
--text-sm: 0.875rem; /* 14px — secondary text */
--text-base: 1rem; /* 16px — body */
--text-lg: 1.25rem; /* 20px — lead, h4 */
--text-xl: 1.5rem; /* 24px — h3 */
--text-2xl: 1.875rem; /* 30px — h2 */
--text-3xl: 2.25rem; /* 36px — h1, page title */
}
Those are named constants — the Tokens as Constants idea from chapter 18, arriving early because font size is the most obvious place to start treating design values like the variables they are. Notice the names describe role and relative size, not pixels. When a heading needs to be bigger you reach for --text-2xl, not 30px. You stop thinking in pixels and start thinking in steps, which is exactly the point.
If you're on Tailwind, you already have a scale — text-sm through text-5xl are right there, derived from a sensible ratio, and you should treat them as the only sizes you're allowed to use. The failure mode on Tailwind isn't the absence of a scale; it's reaching for text-[15px] or text-[17px] arbitrary values the moment something doesn't look right. Don't. If 14px feels too small and 16px feels too big, the answer is almost never 15px — it's to fix the spacing or weight around it. Every arbitrary-value escape hatch is a tiny defection from the system.
Generating seven sizes doesn't mean using seven. A typical screen lives on three or four: body, a heading, a smaller secondary text, and maybe one large title. The scale is the menu, not the order. Using fewer steps reads as more deliberate, not less.
Pick one ratio, generate five or six sizes, name them as constants, and never type an arbitrary font size again.
Hold the line on this and you've eliminated an entire category of "looks off" before touching anything else.
A scale fixes how big your text is. It does nothing for how it sits on the page — and that's where the next chunk of "amateur" hides. Three numbers carry most of it: line length, line-height, and the space between paragraphs.
The single most common typographic mistake in developer-built UIs is letting body text run the full width of the container. On a wide monitor that means lines of 150 characters or more. Your eye loses the start of the next line on the return sweep, reading slows, and the page feels like a wall.
The comfortable range — called the measure — is roughly 45 to 75 characters per line, with about 66 as the sweet spot for body copy. You don't have to count characters. The ch unit is literally "width of a zero," so you can cap the measure directly:
.prose {
max-width: 65ch; /* ~65 characters — comfortable measure */
}
That one line will do more for the readability of a docs page, a blog post, a long settings description, or a marketing section than any font choice. Anywhere you have a paragraph of real text, it should not be allowed to stretch indefinitely. Cap it.
max-width: 65ch is for blocks of reading text — articles, descriptions, onboarding copy. Don't slap it on cards, tables, nav bars, or full-bleed layout containers; those have their own width logic. The measure governs prose, not the whole page.
Browsers default line-height to about 1.2. For body text that's too tight — descenders nearly touch the line below, and dense paragraphs feel cramped. The rule of thumb:
body { line-height: 1.55; }
h1, h2, h3 {
line-height: 1.15;
text-wrap: balance; /* even line breaks on multi-line headings */
}
The inverse relationship trips people up: as text gets bigger, line-height gets smaller. A 14px caption wants 1.5; a 36px hero wants 1.1. The reason is that line-height is relative — a multiplier on the font size — so a 36px line at 1.5 leaves 18px of air between lines, which on a two-line headline looks like the lines are drifting apart. Set it once per role and forget it.
There's a quiet relationship binding these three numbers together, worth naming so you stop fighting it: line length, line-height, and font size move as a unit. A longer measure needs more leading to stay trackable on the return sweep. A larger size needs a shorter measure to keep character count in the comfortable band. Get all three into their ranges at once — 45 to 75 characters, 1.5-ish on body, headings tight — and prose stops looking like a README and starts looking like a product.
The last piece is vertical rhythm — the gap between blocks. Two failure modes are common: paragraphs jammed together with no separation, or separated by a full blank line and a first-line indent (pick one convention, never both, and on the web it's always spacing). A clean default is to separate paragraphs by roughly the body line-height:
p + p { margin-top: 1em; }
Those gap numbers should come from your spacing system, not from typing 1em everywhere — that's the 8-Point Grid's job, and chapter 6 is where vertical rhythm gets its full treatment. For now, the point is simply: there is a number, it should be consistent, and the browser default is not it.
Capping your text at 65 characters a line will do more for readability than any font you could choose.
Now the part developers most often get backwards. When a heading doesn't feel important enough, the instinct is to add another size to the scale, or grab a third or fourth font weight. Both are wrong. Hierarchy comes from contrast, and the strongest cheap contrast you have is weight against size — not more sizes.
You need exactly two weights for almost any interface: a regular (400) for body text and a bold (600 or 700) for emphasis and headings. That's it. Some designs add a single light or medium in between, but two is enough to build a complete, clear hierarchy. The mistake isn't using too few weights — it's using too many, and using none of them with intent.
Here's the move that does the heavy lifting: size plus weight beats many sizes. A heading that's one step up the scale and bold reads as dramatically more important than a heading that's two steps up but the same weight as the body. You get more hierarchy from --text-xl + font-weight: 600 than from jumping to --text-3xl at regular weight. Two variables, multiplied, do the work of a sprawling size ladder.
Two footguns about weight, both of which read as "amateur" the instant a careful eye lands on them. The first is going too thin: light and ultralight weights (100 to 300) look elegant in a type specimen and turn to mush in a real UI, especially on small text at typical screen brightness. Body text should never sit below 400. If your "elegant" 200-weight headings feel washed out, the weight is the cause, not the color. The second is faking the bold — covered in the warning below — where the browser smears a single loaded weight thicker because you never loaded the real one.
Color does the rest. Once size and weight establish the primary structure, drop secondary text — captions, metadata, helper copy under form fields — to a lighter gray and often a smaller size. You don't make body text bold to emphasize it; you make everything-around-it quieter. Hierarchy is as much about turning things down as turning things up, which is the whole premise of the One Loudest Thing framework in chapter 8.
.label { font-weight: 600; color: #111827; } /* loud */
.value { font-weight: 400; color: #374151; } /* normal */
.helptext { font-weight: 400; color: #6b7280;
font-size: var(--text-sm); } /* quiet */
Three levels, built from two weights, two sizes, and three grays. No fourth font size required.
If you load only the 400 weight of a font and then write font-weight: 700, the browser synthesizes a bold by smearing the strokes thicker — and it looks clumsy in a way most people sense but can't name. Load the real weights you use, usually just 400 and 700. The same goes for faux small caps: don't uppercase-and-shrink; if the face ships real small caps or a 600 weight, use it. Synthetic styling looks synthetic, and it's exactly the detail that triggers the Competence Tax in a sharp reader.
Font choice is where developers freeze, because it feels like the part that needs taste. It needs less than you think. There are two correct answers, and both are nearly foolproof.
Answer one: a good system-font stack. Use the fonts already on the user's machine. They're high quality, load instantly with zero network cost, and look native on every platform. For a huge share of apps this is not a compromise — it's the right call.
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji";
That stack renders as San Francisco on Apple devices, Segoe on Windows, Roboto on Android — each platform's own clean, professional sans-serif. Shipping this instead of the browser's default Times New Roman is, by itself, a visible upgrade for zero effort and zero performance cost.
Answer two: one good webfont. If you want a distinct identity, pick one well-made variable font and use it everywhere. Reliable, free, workhorse choices: Inter, Source Sans, IBM Plex Sans, and similar — typefaces built specifically for screen UI. Pick one, load a variable version so you get every weight from a single file, and you're done.
The rules for not embarrassing yourself with fonts are short:
font-size. If you do bring in a second family, pick one that sits at a similar visual size to your body font so the two feel like they belong together.font-display: swap and self-host or preload so the real font arrives fast.@font-face {
font-family: "Inter";
src: url("/fonts/Inter-var.woff2") format("woff2");
font-weight: 100 900; /* variable: all weights, one file */
font-display: swap; /* show fallback, then swap in */
}
That's the entire decision. System stack for zero-fuss native quality, or one variable webfont for identity. Anything more elaborate is a way to spend taste you said you didn't have — and to introduce risk where the safe options are genuinely good.
One good typeface used everywhere beats two fonts trying to harmonize — the safest font decision is the smallest one.
You can ship every one of these before lunch. Go in order.
Grep your styles for font-size and text-[ arbitrary values. If you find more than six distinct sizes, you don't have a scale — you have an accident. The count itself is the diagnosis.
Pick a ratio (1.2 is a safe default), generate five or six sizes from a 16px base, and write them as CSS variables or confirm you're using your framework's built-in scale. Success: every size in the app maps to a named step.
Add max-width: 65ch to your article, description, and long-text containers. Success: no line of body text runs past ~75 characters on a wide screen.
Body to 1.5–1.6, headings to 1.1–1.25. Stop accepting the browser default of 1.2 on everything. Success: paragraphs feel roomy, headings read as tight single units.
Body at 400, headings and emphasis at 600 or 700, secondary text in a lighter gray. Delete any third or fourth weight. Success: your most important heading is one scale step up AND bold — not just bigger.
Drop in the system-font stack, or self-host one variable webfont with font-display: swap. Success: nothing renders in the browser default serif anymore, and there's no flash of invisible text.
Do those six things and your app will look noticeably more finished by the afternoon, with not one decision that required an eye. That's the quiet truth about type: it rewards discipline, not talent.
Type doesn't reward talent. It rewards arithmetic — and that's a game you already know how to win.