Typography

✍️ Chapter 5 — Typography

Typography is the craft of arranging type so that text is legible, readable, and appealing. In CSS, typography properties give you fine-grained control over every aspect of how text is rendered — the font family, the size, the weight, the spacing between lines and letters, the alignment, and more. Getting typography right is one of the highest-leverage skills in web design: good type can elevate a simple layout; poor type can undermine an otherwise well-built page.

1 — font-family

The font-family property specifies which typeface to use. Because a given font may not be installed on every user's device, you always supply a font stack — a comma-separated list of fonts in order of preference. The browser works through the list and uses the first one it finds. The list should always end with a generic family as the ultimate fallback.

🎨 CSS — Font stacks
/* Serif stack — classic, editorial feel */ body { font-family: Georgia, 'Times New Roman', Times, serif; } /* Sans-serif stack — clean, modern, screen-optimised */ body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } /* System UI stack — uses the OS's native interface font */ body { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } /* Monospace stack — for code and terminal output */ code { font-family: 'Courier New', Consolas, 'Lucida Console', monospace; } /* Font names with spaces must be quoted */ h1 { font-family: 'Times New Roman', serif; }

Generic font families

serif
The quick brown fox
Georgia, 'Times New Roman', serif
sans-serif
The quick brown fox
Arial, Helvetica, sans-serif
monospace
The quick brown fox
'Courier New', Consolas, monospace
font-family is inherited. Setting it once on body cascades to every element on the page. You only need to override it where you want a different font — for example, using a monospace stack on code and pre elements.

2 — Web Fonts

Web-safe fonts (those reliably installed on most operating systems) are limited. For anything beyond the standard handful you have two options: load a font from a service like Google Fonts, or host the font files yourself with @font-face.

Google Fonts — the quickest route

🌐 HTML — Loading a Google Font via <link>
<head> <!-- Add this inside <head>, before your stylesheet --> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;700&family=Merriweather:ital,wght@0,400;1,400&display=swap" rel="stylesheet"> <link rel="stylesheet" href="styles.css"> </head>
The two preconnect links tell the browser to start the connection to Google's servers early — this improves loading speed.
🎨 CSS — Using a Google Font after loading it
body { font-family: 'Lato', sans-serif; /* fallback if font fails */ } h1, h2, h3 { font-family: 'Merriweather', Georgia, serif; }

Self-hosted fonts with @font-face

🎨 CSS — @font-face for self-hosted fonts
/* Define the font — at the top of your stylesheet */ @font-face { font-family: 'MyFont'; src: url('fonts/myfont.woff2') format('woff2'), url('fonts/myfont.woff') format('woff'); font-weight: normal; font-style: normal; } /* Use it like any other font */ body { font-family: 'MyFont', sans-serif; }
woff2 is the modern, compressed format supported by all current browsers. Always list it first; include woff as a fallback for slightly older browsers.
💡 Start with system fonts or Google Fonts. Self-hosted fonts give you full control and eliminate a third-party dependency, but require you to obtain and host the font files. Google Fonts is the easiest starting point — it is free, fast, and has hundreds of quality typefaces.

3 — font-size

CSS offers several units for font size. Understanding the difference between absolute and relative units is one of the most important concepts in typography.

🎨 CSS — Font size units
/* px — absolute pixels. Precise but ignores user browser settings */ p { font-size: 16px; } /* rem — relative to the root element (<html>) font size. Default root is 16px, so 1rem = 16px, 1.5rem = 24px. Best choice for font sizes — scales with user preferences. */ p { font-size: 1rem; } /* 16px */ h2 { font-size: 1.5rem; } /* 24px */ h1 { font-size: 2rem; } /* 32px */ /* em — relative to the PARENT element's font size. Useful for spacing that should scale with its own text, but can be confusing when nested. */ .caption { font-size: 0.85em; } /* 85% of parent size */ /* % — same as em for font-size (100% = parent's size) */ .small { font-size: 80%; } /* Keywords — browser-defined sizes (avoid in production) */ p { font-size: medium; } /* small · medium · large · x-large etc. */
0.75rem
Caption / fine print — 12px equivalent
0.875rem
Small text / labels — 14px equivalent
1rem
Body text — 16px equivalent (default)
1.25rem
Lead paragraph — 20px equivalent
1.5rem
h3 heading — 24px
2rem
h2 heading — 32px
3rem
h1 heading — 48px
⚠️ Avoid setting a px font-size on the <html> or <body> element. Many users increase their browser's default font size for accessibility reasons. If you override it with a fixed pixel value (e.g. html { font-size: 16px; }), their preference is ignored and rem units stop scaling correctly. Set font sizes in rem and leave the root untouched.

4 — font-weight

Font weight controls the thickness (boldness) of the characters. Values run from 100 (thinnest) to 900 (heaviest) in steps of 100, with normal equalling 400 and bold equalling 700. Not all fonts include every weight — if a requested weight is unavailable, the browser picks the closest one.

🎨 CSS — font-weight values
.thin { font-weight: 100; } /* Thin (Hairline) */ .light { font-weight: 300; } /* Light */ .regular { font-weight: 400; } /* Regular (same as "normal") */ .medium { font-weight: 500; } /* Medium */ .semibold { font-weight: 600; } /* SemiBold / DemiBold */ .bold { font-weight: 700; } /* Bold (same as "bold") */ .extrabold { font-weight: 800; } /* ExtraBold */ .black { font-weight: 900; } /* Black (Heavy) */ /* Keyword equivalents */ p { font-weight: normal; } /* = 400 */ strong { font-weight: bold; } /* = 700 */
300 light
The five boxing wizards jump quickly.
400 normal
The five boxing wizards jump quickly.
500 medium
The five boxing wizards jump quickly.
600 semibold
The five boxing wizards jump quickly.
700 bold
The five boxing wizards jump quickly.
900 black
The five boxing wizards jump quickly.

5 — font-style

The font-style property renders text in italic (slanted) or normal (upright) form. Most use cases involve just two values.

🎨 CSS — font-style
em, .italic { font-style: italic; } /* slanted — uses the italic variant of the font */ .normal { font-style: normal; } /* resets italic back to upright */ .oblique { font-style: oblique;} /* mechanically slanted (use italic instead) */ /* Common use: reset italic on a child inside an <em> */ em em { font-style: normal; }
Live examples

font-style: normal — upright text, the default.

font-style: italicslanted text, used for emphasis, citations, and foreign words.

6 — line-height

line-height controls the vertical space between lines of text. It is one of the biggest factors in readability — too tight and the text is exhausting to read; too loose and the eye loses its place. The unitless number form is almost always the right choice.

🎨 CSS — line-height values
/* Unitless number — multiplier of the current font-size (BEST) */ p { line-height: 1.6; } /* 1.6× font-size — inherits well */ /* px — fixed line height regardless of font size (avoid) */ p { line-height: 24px; } /* em / % — relative to font-size but does NOT inherit the multiplier, only the computed px value — causes issues in nested elements */ p { line-height: 150%; } /* Typical values for different contexts */ h1 { line-height: 1.2; } /* headings — tight, letters are large */ p { line-height: 1.7; } /* body text — comfortable reading */ .ui { line-height: 1.4; } /* UI labels — between heading and body */ .caption{ line-height: 1.5; } /* captions — slightly tighter than body */
line-height: 1.2 (tight)

This is what tightly spaced body text looks like. Notice how the lines feel cramped and the eye must work harder to track from one line to the next.

line-height: 1.7 (comfortable)

This is what comfortably spaced body text looks like. The generous line height makes it easy to track from line to line — ideal for long-form reading.

7 — letter-spacing and word-spacing

letter-spacing adds or removes space between individual characters (also called tracking). word-spacing adjusts the space between words. Both accept positive or negative values.

🎨 CSS — Spacing between characters and words
/* letter-spacing — commonly used on headings and labels */ .label { letter-spacing: 0.08em; /* slightly open — good for uppercase labels */ text-transform: uppercase; } .headline { letter-spacing: -0.02em; /* slightly tight — large headings often benefit */ } .spaced-out { letter-spacing: 0.3em; /* very open — decorative use only */ } /* word-spacing — use sparingly */ .spread { word-spacing: 0.2em; }

Tight headline tracking

Normal body text — no letter spacing applied.

OPEN TRACKING ON A SMALL LABEL

💡 Always add letter-spacing when using text-transform: uppercase. All-caps text at normal tracking looks cramped. A value of 0.05em to 0.12em dramatically improves readability. Conversely, never add positive tracking to long paragraphs of body text — it is exhausting to read.

8 — text-align

text-align controls the horizontal alignment of text within its container. It applies to block-level elements and affects all inline content within them.

🎨 CSS — text-align values
.left { text-align: left; } /* default for LTR languages */ .center { text-align: center; } /* headings, hero text, captions */ .right { text-align: right; } /* numbers in table cells */ .justify { text-align: justify; } /* stretches lines to fill width — use sparingly */
⚠️ Use justify with care. Justified text creates "rivers" — irregular gaps between words — especially at narrower column widths. It can look polished in print but often hurts readability on screen. Left-align is the safe default for body text in almost every situation.

9 — text-decoration and text-transform

text-decoration

Controls underlines, overlines, and strikethroughs. Most commonly used to remove the default underline from links.

🎨 CSS — text-decoration
a { text-decoration: none; } /* remove link underline */ a:hover { text-decoration: underline; } /* restore on hover */ .deleted { text-decoration: line-through; } /* strikethrough */ .overlined { text-decoration: overline; } /* line above text */ /* Control the underline style and colour independently */ .fancy-link { text-decoration: underline; text-decoration-color: #4f8ef7; text-decoration-style: wavy; /* solid · dashed · dotted · wavy · double */ text-underline-offset: 4px; /* gap between text and underline */ }

text-transform

Changes the capitalisation of text in CSS — without altering the actual HTML content.

🎨 CSS — text-transform
.upper { text-transform: uppercase; } /* ALL CAPS — labels, nav, badges */ .lower { text-transform: lowercase; } /* all lower case */ .capitalise { text-transform: capitalize; } /* Title Case — first letter of each word */ .none { text-transform: none; } /* reset */

uppercase with letter-spacing — section labels

capitalize — Every Word Gets An Initial Capital

line-through — was £49.99

wavy underline with a custom colour

10 — text-shadow

text-shadow adds a shadow behind text. The syntax is: horizontal-offset vertical-offset blur-radius colour. Multiple shadows can be stacked, separated by commas.

🎨 CSS — text-shadow
/* offset-x | offset-y | blur | colour */ h1 { text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); } /* Subtle lift — common on light headings over images */ .hero-title { text-shadow: 0 2px 8px rgba(0, 0, 0, 0.6); } /* Glow effect — coloured blur with no offset */ .glow { text-shadow: 0 0 12px rgba(79, 142, 247, 0.8); } /* Multiple shadows — layered for depth */ .deep-shadow { text-shadow: 1px 1px 0 rgba(0,0,0,0.4), 2px 2px 0 rgba(0,0,0,0.3), 3px 3px 0 rgba(0,0,0,0.2); } /* Remove — useful to reset an inherited shadow */ .no-shadow { text-shadow: none; }

Classic drop shadow

Blue glow effect

Layered depth shadow

11 — The font Shorthand

The font shorthand combines font-style, font-weight, font-size, line-height, and font-family into a single declaration. The order is strict, and font-size and font-family are required.

🎨 CSS — font shorthand syntax
/* font: [style] [weight] size[/line-height] family */ p { font: 1rem/1.7 Georgia, serif; /* size: 1rem | line-height: 1.7 | family: Georgia, serif */ } h1 { font: 700 2.5rem/1.2 'Merriweather', Georgia, serif; /* weight: 700 | size: 2.5rem | line-height: 1.2 | family */ } .caption { font: italic 300 0.85rem/1.5 system-ui, sans-serif; /* style: italic | weight: 300 | size/line-height | family */ }
The line-height is appended to the font-size with a forward slash: 1rem/1.7. Omitting any optional part (style, weight, line-height) leaves it at its default value.
Shorthand resets omitted values. Using the font shorthand resets any font property you do not include back to its initial value. This catches beginners out — if you set font-weight: 700 earlier and then write font: 1rem Georgia, serif (no weight), the weight resets to 400. When in doubt, use the longhand properties individually.

12 — Quick Reference

PropertyWhat it controlsKey values
font-familyTypeface and fallback stackFont names (quoted if spaces), generic: serif sans-serif monospace
font-sizeText size1rem (prefer) · px · em · %
font-weightThickness / boldness100900 · normal (400) · bold (700)
font-styleUpright vs italicnormal · italic · oblique
fontShorthand for the above four + line-height[style] [weight] size[/lh] family
line-heightVertical space between linesUnitless number (e.g. 1.6) — preferred
letter-spacingSpace between charactersem values — positive opens, negative tightens
word-spacingSpace between wordsem or px
text-alignHorizontal alignmentleft · center · right · justify
text-decorationUnderline, strikethrough, overlinenone · underline · line-through · overline
text-transformCapitalisationuppercase · lowercase · capitalize · none
text-shadowShadow behind textx y blur colour — comma-separated for multiple

✏️ Exercises

Typography is best learned by doing. Each exercise below builds a real UI pattern — compare your result to the description and tweak until it reads well.

Exercise 1
Create a typographic scale. Build a page that displays all six heading levels (h1 through h6) and a paragraph. Set each heading to a rem-based size using a scale of your choice (e.g. 3rem, 2.25rem, 1.75rem, 1.4rem, 1.15rem, 1rem). Give all headings a weight of 700 and set the paragraph to 1rem with a line-height of 1.7. Use a single grouped selector for the shared heading styles.
Hint: group headings with h1, h2, h3, h4, h5, h6 { font-weight: 700; }, then write individual rules for each size.
CSS
* { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: Georgia, serif; padding: 40px; color: #e2e4ec; background: #0d0d0d; } h1, h2, h3, h4, h5, h6 { font-weight: 700; line-height: 1.2; margin-bottom: 0.5rem; color: #ffffff; } h1 { font-size: 3rem; } h2 { font-size: 2.25rem; } h3 { font-size: 1.75rem; } h4 { font-size: 1.4rem; } h5 { font-size: 1.15rem; } h6 { font-size: 1rem; } p { font-size: 1rem; line-height: 1.7; color: #a0a4b8; margin-bottom: 1rem; }
Exercise 2
Style a navigation bar's links. Create a <nav> with five <a> links. Remove the default underline, set them to uppercase with 0.08em letter-spacing and a 600 font-weight. On hover, add an underline with a custom colour and a 3px offset. Make sure the links are not blue by default — choose a colour that fits a dark navigation bar.
Hint: use text-decoration: none on the base a rule, then add it back on a:hover. Use text-underline-offset to control the gap.
CSS
nav { background: #12141e; padding: 16px 32px; display: flex; gap: 28px; } nav a { font-family: system-ui, sans-serif; font-size: 0.78rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; text-decoration:none; color: #a0a4b8; } nav a:hover { color: #ffffff; text-decoration: underline; text-decoration-color: #4f8ef7; text-underline-offset: 4px; }
Exercise 3
Build an article layout that demonstrates good typography choices. Create a page with a large h1 heading, a short lead paragraph (slightly larger and lighter weight than body), body paragraphs with comfortable line-height, and a small caption below a blockquote. Use rem units for all sizes. The heading should have tight line-height and slightly negative letter-spacing; the body should have generous line-height.
Hint: try h1 { font-size: 2.8rem; line-height: 1.15; letter-spacing: -0.02em; } and p { font-size: 1.05rem; line-height: 1.75; }. For the lead paragraph, bump it to 1.2rem with font-weight: 300.
CSS
body { font-family: Georgia, serif; background: #0d0d0d; color: #e2e4ec; padding: 60px 20px; } .article { max-width: 660px; margin: 0 auto; } .article h1 { font-size: 2.8rem; line-height: 1.15; letter-spacing: -0.02em; margin-bottom: 1.2rem; } .lead { font-size: 1.2rem; font-weight: 300; line-height: 1.6; color: #c9a8ff; margin-bottom: 2rem; } .article p { font-size: 1.05rem; line-height: 1.75; color: #a0a4b8; margin-bottom: 1.2rem; } blockquote { border-left: 3px solid #4f8ef7; padding-left: 1.2rem; margin: 2rem 0; font-style: italic; color: #e2e4ec; } .caption { font-family: system-ui, sans-serif; font-size: 0.78rem; color: #7a7f96; text-transform: uppercase; letter-spacing: 0.07em; margin-top: -0.8rem; }
Exercise 4
Create a hero heading with a text-shadow glow effect. Make a full-width dark <section>, at least 300px tall, centred vertically and horizontally. Place an h1 inside it with a bright colour and a coloured glow shadow (no offset, just blur). Below it, add a subtitle in a lighter weight with a subtler shadow. Experiment with the blur radius until the glow feels right — not too harsh.
Hint: for a glow, set offset-x and offset-y both to 0 — only the blur matters: text-shadow: 0 0 20px rgba(79, 142, 247, 0.8). Layer two shadows for a richer effect: a tight bright one and a wider diffuse one.
CSS
* { box-sizing: border-box; margin: 0; padding: 0; } .glow-hero { background: #0d0d0d; min-height: 360px; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 40px; font-family: Georgia, serif; } .glow-hero h1 { font-size: 3.5rem; font-weight: 700; color: #4f8ef7; letter-spacing:-0.02em; line-height: 1.1; text-shadow: 0 0 8px rgba(79, 142, 247, 1.0), 0 0 30px rgba(79, 142, 247, 0.5); margin-bottom: 1rem; } .glow-hero p { font-size: 1.1rem; font-weight: 300; color: #a0a4b8; text-shadow: 0 0 12px rgba(160, 164, 184, 0.4); }