Typography basics
Status: 🟩 COMPLETE Last updated: 2026-06-19 Plain-English tagline: The handful of typography decisions — type scale, line height, font choice, weight contrast — that make text feel professional, readable, and trustworthy without needing to study design for a year.
In plain English
Most webapps live or die on text. People READ on the screen — headings, body, buttons, labels, error messages. If the text is hard to read, the app feels hard. If the text feels coherent and confident, the app feels coherent and confident, even when the underlying functionality is plain.
Typography is the discipline of arranging text. For an engineer building a webapp without formal design training, you don’t need to study it deeply — but a handful of decisions move you from “amateur” to “competent” with surprisingly little effort:
- Pick one or two fonts. Three is too many. Stick.
- Use a type scale — a small, fixed set of font sizes for the entire app.
- Set line height carefully. Body text: 1.5. Headings: 1.1-1.25.
- Use weight contrast instead of color or italic for emphasis.
- Cap line length at 60-80 characters for body text.
- Set base size to 16 px. Don’t go smaller for body.
- Add letter-spacing to small uppercase labels; don’t touch body text spacing.
Apply those seven, and your app’s text will look more polished than 80% of solo-developer projects. Add a touch of restraint, and you’ve covered most of what typography offers a non-designer.
The 2026 stack makes this easier: Tailwind gives you a thoughtful default type scale; Google Fonts and the next/font system make typography free and fast; shadcn/ui ships with reasonable typography baked in. You’re often a few tweaks away from “looks designed.”
Why it matters
Three concrete reasons typography is high-leverage:
-
Most of your app IS text. Buttons, labels, headings, paragraphs, errors, tooltips — text dominates the interface. Tuning text affects every screen.
-
Polish compounds. Most products feel “engineer-built” because the typography is whatever the framework defaulted to. Spending 30 minutes choosing a font + type scale moves the perceived quality bar dramatically.
-
Readability is accessibility. Type that’s too small, too tight, or poorly contrasted excludes users with low vision or dyslexia. Good typography is also good a11y.
The trade-off: typography rewards restraint. The temptation is to use 5 fonts, 12 sizes, italic + bold + colored variants everywhere. The professional move is the opposite — fewer variations, more consistency.
The seven decisions (in detail)
1. Pick one or two fonts
For 95% of webapps: one font for everything is plenty. A well-chosen sans-serif (Inter, Geist, system-ui) handles body, headings, code labels, buttons.
If you want contrast between body and headings, the safe move is one sans-serif + one serif (e.g. Inter for body + a humanist serif for headings). Stop there.
Three fonts is almost always a mistake. The screen feels noisy; nothing feels like the “primary.”
Good defaults in 2026:
| Use case | Suggested font |
|---|---|
| Default sans-serif | Inter (free, comprehensive, neutral) |
| Modern technical look | Geist (Vercel’s, ships with Next.js) |
| System default (zero cost, native) | system-ui / -apple-system, BlinkMacSystemFont, ... |
| Serif for headings | Source Serif Pro, Crimson Pro, Libre Caslon |
| Display / brand | Cal Sans, Playfair Display, Fraunces |
| Monospace (code) | JetBrains Mono, Geist Mono, Fira Code |
For Bible Quest-style projects, Inter + a single serif for verse text (if you want gravitas on scripture) is a strong starting point. Or just Inter everywhere.
2. Use a type scale
A type scale is the small, fixed set of font sizes you’ll use across the app. Instead of “this label is 13 px, this one is 14 px, this one is 12 px,” you pick maybe 6-8 sizes and stick to them.
A typical type scale:
| Token | Size | Usage |
|---|---|---|
text-xs | 12 px | Captions, fine print, badges |
text-sm | 14 px | Secondary text, small buttons, dense lists |
text-base | 16 px | Body text (default) |
text-lg | 18 px | Slightly emphasized body, large links |
text-xl | 20 px | Card titles, small headings |
text-2xl | 24 px | Section headings |
text-3xl | 30 px | Page subheadings |
text-4xl | 36 px | Page titles |
text-5xl+ | 48 px+ | Hero headlines, landing page bombast |
This is exactly Tailwind’s default scale. You get it for free.
Most of your app uses text-sm (UI chrome) and text-base (body). Heading sizes appear less often. Avoid ad-hoc sizes (text-[15px]) — they break the rhythm.
A modular scale (like 1.250 — major third) generates sizes mathematically. Tailwind’s defaults approximate this. You don’t need to compute your own.
3. Line height matters more than you think
Line height (CSS line-height) is the vertical space between lines of text. Too tight = cramped. Too loose = paragraphs feel disconnected.
| Content | Line height |
|---|---|
| Body text (paragraphs) | 1.5 to 1.65 (Tailwind: leading-relaxed or leading-loose) |
| Compact UI text (buttons, labels) | 1.2 to 1.4 (Tailwind: leading-snug or leading-tight) |
| Headings | 1.1 to 1.25 (Tailwind: leading-tight) |
The single biggest improvement to a “developer-default” looking page: bump body text from 1.0 (which most browsers default to) to 1.5. The text becomes immediately easier to read.
Tailwind’s prose class (from @tailwindcss/typography) handles this and many other body-text concerns. For long-form content (Bible passages, blog posts, documentation), <article className="prose"> is a one-line win.
4. Use weight, not color, for emphasis
Two ways to make text stand out: change its weight (light, regular, medium, semibold, bold) or its color.
Weight contrast is the professional move:
- Body text in
font-normal(400) - A key term within:
font-semibold(600) orfont-medium(500) - Headings:
font-semiboldorfont-bold(700)
Color contrast for emphasis usually looks amateur:
- Body in black, emphasized words in red? Looks like a 1998 GeoCities page
- Italic? Subtle but easily lost on screen
- Underline? Confuses with links
The exception: muted text for de-emphasis (text-muted-foreground in shadcn — usually a 40-60% gray). De-emphasizing by color works; emphasizing by color usually doesn’t.
5. Cap line length
Reading line lengths over ~80 characters tires the eye. Lines under ~45 characters break too often. The sweet spot for body text: 60-80 characters per line (about 600-800 px at 16 px font).
Set max-width on body content:
.prose {
max-width: 65ch; /* 65 characters wide */
}ch is a CSS unit equal to the width of the “0” character — line-length-aware sizing for free.
For Tailwind: max-w-prose gives you 65ch. Use it on article content, long form, anywhere users read more than a single line.
6. Base size 16 px (or larger)
The standard web base size for body text is 16 px. Smaller than that is hard to read on phones, requires zooming for low-vision users, and feels cramped.
If you must go smaller for UI chrome (captions, footnotes), 14 px is the floor. Below 12 px is uncomfortable.
Mobile-specifically: some sites bump base to 17 px or 18 px for body, recognizing that phone viewing distances are different from desktop.
7. Letter spacing — tighten headings, leave body alone
Letter spacing (CSS letter-spacing) is the space between characters. Small adjustments make a difference:
- Body text — leave at default (0). Adjusting it harms readability.
- Large headings — slightly tighter often looks better (
tracking-tightin Tailwind, ≈ -0.025em). - Small uppercase labels — slightly looser improves readability (
tracking-widetotracking-wider, ≈ +0.025em to +0.05em).
<h1 className="text-4xl font-semibold tracking-tight">Welcome</h1>
<p className="text-base leading-relaxed">A normal paragraph.</p>
<span className="text-xs uppercase tracking-wider text-muted-foreground">
Category
</span>A concrete example: a polished homepage hero
Compare a “default” engineer hero:
<div>
<h1>Bible Quest</h1>
<p>A modern way to study scripture daily.</p>
<button>Get started</button>
</div>With typography decisions applied:
<div className="max-w-prose mx-auto py-24 text-center">
<h1 className="text-5xl font-semibold tracking-tight text-foreground">
Bible Quest
</h1>
<p className="mt-6 text-lg leading-relaxed text-muted-foreground">
A modern way to study scripture daily.
</p>
<button className="mt-8 px-6 py-3 text-base font-medium rounded-md bg-primary text-primary-foreground">
Get started
</button>
</div>What changed:
- Single font (the default sans-serif loaded via
next/font) - Heading uses
text-5xl(48 px) — big enough to be a hero, not absurd font-semiboldnotfont-bold— more refinedtracking-tighton the heading — large text wants tighter spacing- Body in
text-lgnottext-base— slightly larger for hero context leading-relaxedon body — comfortable readingtext-muted-foregroundon the body — de-emphasizes vs the headingmt-6,mt-8for spacing — rhythmic, not arbitrarymax-w-prose— keeps the line length sane
No new tools, no new colors. Just typography decisions. Hours of design work compressed into about 7 lines.
Loading custom fonts in Next.js
next/font (since Next 13) makes custom fonts trivially fast and CLS-free:
// app/layout.tsx
import { Inter } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={inter.variable}>
<body className="font-sans">{children}</body>
</html>
);
}tailwind.config.ts:
export default {
theme: {
fontFamily: {
sans: ["var(--font-inter)", "system-ui", "sans-serif"],
},
},
};What this does:
- Downloads Inter at build time (no runtime network roundtrip)
- Self-hosts it (Vercel serves it; no external request to Google Fonts at page load)
- Applies a CSS variable +
font-familyfallback - Eliminates the FOUT / FOIT (flash of unstyled / invisible text)
For a second font (a serif for headings):
import { Inter, Crimson_Pro } from "next/font/google";
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
const crimson = Crimson_Pro({ subsets: ["latin"], variable: "--font-serif" });
// Apply both class names to <html>Then in Tailwind, define font-serif: ["var(--font-serif)", "serif"] and use font-serif on the elements you want.
Variable fonts — a quiet 2020s win
Most modern web fonts are now variable fonts — a single file contains many weights, widths, and styles. Instead of loading 4 files (Regular, Bold, Italic, Bold Italic), you load one and request weights as needed.
Practical benefits:
- Smaller total payload (one file vs many)
- Smoother weight transitions
- More design flexibility (any weight from 100 to 900, not just preset stops)
Inter, Geist, Source Sans, Crimson Pro are all variable. next/font handles them automatically; you just use font-weight: 500 and the right weight is selected.
Common gotchas
-
Three fonts looks worse than one. Adding fonts feels like adding personality; usually it just adds noise. Restraint wins.
-
Default browser line-height (~1.2) is too tight for body text. Always set
leading-relaxed(1.625) or similar on paragraphs. The improvement is dramatic. -
Ad-hoc sizes shatter the type scale. Don’t reach for
text-[15px]because “14 is too small but 16 is too big.” Either pick from the scale or extend the scale once for the whole app. -
Justified text breaks readability.
text-align: justifylooks crisp in a typewritten book and broken on a small screen with weird word spacing. Leave it left-aligned (or center for short headings). -
Italic doesn’t render well at small sizes. A 12-px italic word in body text looks blurry. Use bold for emphasis at small sizes.
-
All-caps body text is hard to read. Word shapes disappear; brain has to spell out each word. Reserve all-caps for SHORT labels (3-15 characters).
-
Line height in em vs unitless.
line-height: 1.5(unitless) scales correctly withfont-size.line-height: 1.5emdoesn’t compose well in nested elements. Use unitless. -
Letter-spacing on body text degrades readability. Anything beyond default is too much for paragraphs. Reserve for short emphasis.
-
Centered body text is uncomfortable. Each line starts at a different x-position; the eye has to find the start of every line. Center HEADINGS, not paragraphs.
-
Hairline weights (100, 200) at small sizes are unreadable. A 12-px font-weight-200 is anemic. Stick to 400+ for body; 500+ for UI.
-
Auto-hyphenation can help long words on narrow columns.
hyphens: auto(withlangattribute set) lets the browser break long words at hyphens. Especially useful on phones. -
text-overflow: ellipsisneedswhite-space: nowrapandoverflow: hidden. Forgetting any of the three breaks the ellipsis. Tailwind’struncateutility does all three. -
Tabular numbers matter for tables.
font-variant-numeric: tabular-numsmakes all digits the same width, so numbers in a column align. Especially useful for financial / data tables. -
font-display: swapreduces invisibility. Tells the browser to use a fallback font immediately, then swap to the custom one when loaded.next/fonthandles this; if you’re loading fonts manually, set it. -
Some custom fonts ship without certain weights/styles. Variable fonts solve this; static-weight fonts may have only Regular + Bold. Verify before designing.
-
text-rendering: optimizeLegibilitywas a 2010s thing. Modern browsers handle this automatically; setting it can actually slow rendering. Don’t bother. -
Don’t use
<center>or<font>tags. They’ve been deprecated for two decades. Use CSS. -
Don’t disable smooth font rendering.
-webkit-font-smoothing: antialiasedis a holdover; modern browsers render fonts fine without it. Some Mac-only ports of fonts look thinner with antialiased; test before applying. -
Don’t use Lorem Ipsum past wireframing. Real content reveals real problems with type sizes, line lengths, and copy length. Use actual sample data early.
-
proseclass behaviors can collide. Tailwind’s@tailwindcss/typographyplugin styles all child elements (headings, links, lists). If you customize, be aware of the cascade. -
Tracking-tight on small text is illegible. It’s a heading-only optimization. Body text at default tracking, please.
-
Mobile users read at greater distance than designers. Type sizes that look big on a 27” monitor at 24 inches away look small on a phone at 18 inches away. Test on real phones.
-
Font fallbacks matter when custom fonts fail. A weight-700 word in your custom font looks fine; the fallback might be lighter, causing layout shift. Set
size-adjust,ascent-overridein@font-faceto match metrics.next/fontdoes this automatically with theadjustFontFallbackoption. -
emvsremchoice matters.remis relative to the root font size;emis relative to the parent. For consistent scaling, preferrem. Useemfor component-relative spacing (e.g., margin around a heading that scales with the heading). -
Don’t use
text-align: justifyon mobile. Already a gamble on desktop; on narrow screens, the word spacing becomes grotesque. -
Punctuation and quotes matter. Use smart quotes (” ” ’ ’) not straight (” ”). Em dashes (—) and en dashes (–) for ranges. Most type-aware text editors do this automatically; Markdown often doesn’t.
-
font-feature-settingsopens advanced features. Ligatures, stylistic sets, slashed zeros. Most variable fonts ship with thoughtful defaults; you rarely need to touch these. When you do, the syntax isfont-feature-settings: "salt" on;and similar. -
Dark mode and typography interact. Heavy text weights look heavier on dark backgrounds (light-on-dark optically thickens). Consider a 50-unit lighter weight for body on dark mode (500 → 400 sometimes).
-
AI-generated copy is often the wrong length. Designs that look balanced with AI placeholder text may break with real, longer (or shorter) human writing. Test with realistic content length.
Tools and resources
- Tailwind type scale — the default scale you’re already using
- Modularscale.com — generate custom type scales
- Type-scale.com — interactive playground
- Fontjoy — font pairing suggestions
- Google Fonts — biggest free font library
- Bunny Fonts — GDPR-compliant Google Fonts mirror
- Inter — the canonical modern UI font
- Geist — Vercel’s house font; ships with Next.js
- Practical Typography — Matthew Butterick’s free book
See also
- CSS 🟩 — the underlying mechanics
- Tailwind 🟩 — the practical layer most projects use
- UX principles 🟩 — typography serves UX
- Accessibility (design view) 🟩 — typography is a11y
- Color theory for devs 🟩 — typography + color together create hierarchy
- Mobile-first thinking 🟩 — type sizing for small screens
- Dark mode design 🟥
- ui 🟩 — its typography defaults are well-tuned
- HTML 🟩 — semantic headings matter for typography hierarchy
- Glossary: Typography, Type scale, Variable font
Sources
- Practical Typography (Matthew Butterick) — the most-recommended free book on typography for non-designers
- Refactoring UI — Adam Wathan + Steve Schoger; practical typography decisions for developers
- Web Typography (Richard Rutter)
- Tailwind — Typography
- Next.js —
next/font - MDN —
font-feature-settings— advanced opentype features - Variable Fonts Guide — Google’s guide