Typography in CSS
Master the core CSS properties for controlling text appearance — font family, size, weight, style, line-height, spacing, alignment, decoration, and an introduction to using web fonts.
Typography is arguably the highest-leverage design skill for a developer — good type choices alone can make a plain page look professional, and bad ones can undermine an otherwise well-built layout. This lesson covers the full toolkit CSS gives you for controlling how text looks and reads.
font-family and font stacks
font-family sets the typeface. You should always provide a list of fonts, called a font stack, rather than a single name — the browser tries each one in order and falls back to the next if a font isn't installed or hasn't loaded, ending in a generic family (serif, sans-serif, monospace, cursive, fantasy) as a final guaranteed fallback.
Font names with spaces must be quoted: font-family: 'Times New Roman', serif;. A well-built stack considers what's likely already installed on Windows, macOS, and Linux, or explicitly loads a web font so you're not relying on the system at all.
font-size, font-weight, font-style
font-size sets the text size — usually in rem, as covered in the units lesson. font-weight controls boldness, from 100 (thinnest) to 900 (boldest) in multiples of 100, or keywords like normal (400) and bold (700); a font-family must actually include a weight for the browser to render it — otherwise it fakes ('synthesizes') bold, which often looks worse. font-style sets italic, oblique, or normal.
line-height — unitless vs. unit values
line-height controls the vertical space a line of text occupies (roughly, the distance between baselines). It can be set unitless (e.g. line-height: 1.5) or with a unit (e.g. line-height: 24px or 1.5em). Unitless is almost always preferred: a unitless value is a multiplier of the element's own font-size, and because it's recalculated fresh at each descendant rather than passed down as a fixed computed pixel value, it scales correctly if a nested element has a different font-size. A unit-based line-height set on a parent gets inherited as that exact fixed value, which can cause cramped or excessive spacing in children with different font sizes.
letter-spacing and word-spacing
letter-spacing adjusts the space between individual characters (tracking) — small negative values can tighten large headings, small positive values can improve readability of all-caps text. word-spacing adjusts the space between words specifically. Both accept length values (commonly em for proportional scaling with font-size) and are used sparingly — overusing either hurts readability.
text-align, text-decoration, text-transform
text-align controls horizontal alignment within its container: left, right, center, justify (stretches lines to fill the full width — use cautiously, it can create awkward gaps). text-decoration adds or removes lines like underline, overline, line-through, or none (commonly used to remove the default underline from links). text-transform changes capitalization for display purposes without altering the actual underlying text: uppercase, lowercase, capitalize.
text-shadow
text-shadow adds a shadow behind text, specified as horizontal offset, vertical offset, blur radius, and color: text-shadow: 2px 2px 4px rgba(0,0,0,0.3);. Multiple shadows can be comma-separated for layered effects.
Web fonts, briefly
To use a typeface that isn't installed on the visitor's device, you load a web font — either through a service like Google Fonts (which gives you a <link> tag or an @import that generates the CSS for you), or by self-hosting font files and declaring them yourself with @font-face, specifying the font's name, file source, and which weights/styles it covers. Once declared, you reference that name normally in font-family. Web fonts add load time, so it's good practice to limit how many weights and families you load, and to always keep a system-font fallback in your stack in case the web font fails to load.
Core typography properties
font-display: swap tells the browser to show fallback text immediately and swap in the web font once it loads, avoiding invisible text while waiting. Always end a font stack with a generic family (serif, sans-serif, monospace) as the ultimate fallback.
body {
font-family: 'Inter', -apple-system, 'Segoe UI', Roboto, sans-serif;
font-size: 1rem;
font-weight: 400;
line-height: 1.6; /* unitless — preferred */
color: #1a1a1a;
}
h1 {
font-size: 2.5rem;
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.15;
}
.eyebrow {
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: 0.875rem;
font-weight: 600;
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Loading a self-hosted web font */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-variable.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}Code Examples
This 'system font stack' uses each operating system's native UI font (San Francisco on macOS/iOS, Segoe UI on Windows, Roboto on Android) for a fast, native-feeling look with zero font-loading cost.
Because unitless line-height is inherited as the raw multiplier (not a computed pixel value), each descendant recalculates it against its own font-size — this is exactly why unitless is recommended.
A soft, semi-transparent shadow can improve legibility of light text over a busy background image without looking heavy-handed.
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI',
Roboto, Helvetica, Arial, sans-serif;
}.article {
line-height: 1.6; /* unitless multiplier */
font-size: 16px;
}
.article h2 {
font-size: 28px;
/* line-height: 1.6 is RE-COMPUTED here as 1.6 x 28px = 44.8px,
not inherited as a fixed 25.6px (1.6 x 16px) from the parent. */
}.hero-title {
color: white;
text-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
}Important Rules
Always include a font stack with a generic fallback family, not just one font name.
Prefer unitless line-height so it recalculates correctly for descendants with different font sizes.
font-weight values must match weights actually provided by the loaded font, or the browser may synthesize a fake bold/italic that looks poor.
text-transform only changes visual capitalization, not the underlying text content or accessibility tree text.
Use letter-spacing and word-spacing sparingly — small adjustments help, large ones hurt readability.
Typography as a design system
Production sites typically define a small, consistent type scale (a fixed set of font-size and line-height pairs, like 0.875rem/1.4, 1rem/1.6, 1.5rem/1.3, 2.5rem/1.15) rather than choosing sizes ad hoc per component. Combined with a limited font-weight palette (e.g. only 400 and 700), this keeps a site's text feeling cohesive and loads fewer font files, improving performance.
body { font-family: 'Inter'; } /* no fallback */ body { font-family: 'Inter', -apple-system, 'Segoe UI', sans-serif; }
With no fallback, if the 'Inter' font fails to load (slow network, blocked request) the browser falls back to its own arbitrary default rather than a font you chose, and text may render inconsistently across visits..card p { line-height: 24px; } /* fixed unit, set on a container with mixed font sizes inside */.card p { line-height: 1.5; } /* unitless, scales per element */
A pixel-based line-height inherits as that exact fixed value into children, which can look cramped on larger nested text (like a heading) or overly loose on smaller text (like a caption). Unitless line-height avoids this by recalculating per element.
[Concept Matching] Match each typography behavior to the correct property: (1) removes the underline from a link, (2) changes displayed text to all uppercase without altering the HTML content, (3) sets a value that scales proportionally instead of inheriting a fixed pixel amount into children with different font sizes. A. (1) text-transform, (2) text-decoration, (3) letter-spacing B. (1) text-decoration, (2) text-transform, (3) unitless line-height C. (1) font-style, (2) text-align, (3) word-spacing D. (1) text-decoration, (2) font-variant, (3) font-size
B. (1) text-decoration, (2) text-transform, (3) unitless line-height — text-decoration: none removes the underline; text-transform: uppercase changes only the visual rendering, not the underlying markup; unitless line-height (e.g. 1.5) is the value that recalculates per element's own font-size rather than inheriting a fixed computed pixel value like a unit-based line-height would.
Style a blog post title: it should use a bold weight, be visually larger than body text, use a tight unitless line-height suitable for headings (around 1.1–1.2), and remove the underline if it happens to also be a link.
h1.post-title { font-size: 2.25rem; font-weight: 700; line-height: 1.15; text-decoration: none; }
Combine font-weight, font-size, line-height, and text-decoration.