Readable First, Pretty Second
Most of what makes a page look designed is line height, line length, and contrast — three properties, none of them exciting. Learn the four ways to write a colour and why the default line-height is wrong for body text.
Most of what makes a page look designed is line height, line length, and contrast — three properties, none of them exciting. Learn the four ways to write a colour and why the default line-height is wrong for body text.
Four ways to write a colour, and when each helps
They all produce the same pixels. The difference is which one you can read and adjust.
color: darkseagreen; /* 1. keyword */color: #3e7f5c; /* 2. hex */color: rgb(62 127 92); /* 3. rgb */color: hsl(150 34% 37%); /* 4. hsl */ /* With transparency */color: #3e7f5c80; /* hex + alpha */color: rgb(62 127 92 / 50%); /* modern syntax */About 140 names, plus transparent and currentColor. Fine for prototyping; too coarse for a real palette.
The most common in the wild. #RGB is shorthand for #RRGGBB, so #fff is white. Unreadable — nobody can tell #3e7f5c from #3e7f6c by eye.
Three channels, 0–255. Slightly more readable than hex and still hard to adjust: making a colour lighter means changing all three numbers.
Hue 0–360, saturation, lightness. The one you can reason about — same hue with different lightness gives a whole palette, and it is a single number to change.
Whichever you use, put the decisions in custom properties rather than repeating values. Chapter 21 builds a whole page this way.
:root { --ink: hsl(150 34% 18%); --muted: hsl(0 0% 39%); --accent: hsl(150 34% 37%); --surface: hsl(40 60% 98%);} body { color: var(--ink); background: var(--surface); }a { color: var(--accent); }--accent rather than --green, for the same reason a class is .warning rather than .red: the name should survive the day you change the colour.
font-family is a list, because fonts go missing
font-family takes a comma-separated stack, and the browser walks it until it finds one the visitor actually has. The last entry must be a generic family, because that one always exists.
/* The system stack: whatever this device uses natively. Zero download, instant render, and it looks right everywhere. */body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;} code, pre { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;} h1, h2 { font-family: Georgia, "Times New Roman", serif;}Quote any family name containing a space. The generic at the end — sans-serif, serif, monospace — is the guaranteed fallback.
System fonts
Nothing to download, so text renders on the first frame with no flash. Looks native on every platform. The right default, and genuinely fine for most sites.
Web fonts
A file the visitor must download before your text appears in it. Worth it for identity, and it costs bytes and a flash of fallback text. Use font-display: swap so the text is readable while it loads.
line-height does more for readability than anything
If you change one property on a page of text, change this one. The browser default is about 1.2, which is right for headings and too tight for body copy — the lines crowd and the eye loses its place returning to the left margin.
body { line-height: 1.6; } /* 1.5–1.7 for body text */h1, h2 { line-height: 1.2; } /* tighter for large text */Write it without a unit. A unitless value is a multiplier that each element applies to its own font size. A value with a unit computes once and then inherits that fixed pixel value to every child, so a large heading inherits a line height meant for 14-pixel text and its lines overlap.
body { font-size: 16px; line-height: 24px; } /* fixed 24px inherited */h1 { font-size: 40px; } /* 40px text, 24px lines — overlapping */ body { font-size: 16px; line-height: 1.5; } /* multiplier inherited */h1 { font-size: 40px; } /* 40px text, 60px lines — correct */Larger text needs proportionally less line height, not more. Long lines need more.
Line length has a right answer
Typography research is unusually settled here: comfortable reading is roughly 45 to 75 characters per line. Beyond that the return sweep to the next line starts failing and readers lose their place.
A full-width paragraph on a 27-inch monitor is around 200 characters. Which is why an unstyled page feels hard to read even though nothing is wrong with it.
.prose { max-width: 65ch; /* about 65 characters of the current font */ margin: 0 auto;}1ch is the width of the "0" character in the current font, so 65ch tracks your font size automatically. Change the size and the measure follows. Use max-width, not width, so narrow screens still use their full space.
rem and em respect a choice the reader made
Sizes can be absolute or relative, and the difference has a real accessibility consequence.
An absolute pixel. Predictable, and it ignores a reader who has set a larger default font size in their browser settings.
Relative to the ROOT font size — 1rem is the browser default, usually 16px, or whatever the reader chose. Use for font sizes and most spacing.
Relative to the CURRENT element's font size, so it compounds through nesting. Excellent for padding that should scale with its own text.
Relative to the parent's corresponding dimension. Mostly for widths.
Relative to character metrics. ch is the one you will use, for measure.
One per cent of the viewport width or height. Handy for full-screen sections; avoid for text, where it ignores the reader entirely.
html { font-size: 100%; } /* respect the reader's setting — do NOT set px here */ body { font-size: 1rem; } /* 16px by default */h1 { font-size: 2.5rem; } /* 40px, scales with the reader */h2 { font-size: 1.75rem; }small { font-size: 0.875rem; } .card { padding: 1.5rem; gap: 1rem; }Typographic defaults worth starting from
- ✓font-family: system-ui with a generic fallback last
- ✓line-height: 1.6 on body, unitless, and ~1.2 on large headings
- ✓max-width around 65ch on anything with paragraphs
- ✓rem for font sizes, so a reader's browser setting still works
- ✓Text contrast of at least 4.5:1 — chapter 22 has the checker
- ✓Two or three font weights, not nine
Key takeaways
- Keywords, hex, rgb, and hsl all produce the same pixels; hsl is the one you can reason about and adjust.
- currentColor follows the element's text colour automatically, through hover and inheritance.
- Name custom properties for their role — --accent, not --green — so the name survives a redesign.
- font-family is a fallback list and the last entry must be a generic family.
- System font stacks download nothing and render on the first frame.
- Every extra weight is another file. Two or three is enough for a whole site.
- line-height is the single highest-value property on a page of text. Use 1.5–1.7 for body copy.
- Write line-height WITHOUT a unit, or large headings inherit a fixed value meant for small text and overlap.
- Comfortable reading is 45–75 characters per line. max-width: 65ch tracks the font size for you.
- rem respects the reader's chosen base font size; px ignores it.
- html { font-size: 62.5% } shrinks that choice to make your arithmetic easier. Do not.
Quick check
Answer these to unlock the next chapter — 3 of 4 to pass. You can retake it anytime.
Answer every question to check.
Make a free account to read on
Every chapter is free — an account is how your progress, XP, and streak follow you from your laptop to your phone, and how you show up on the leaderboard. No payment, no trial.