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.
/* 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
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
<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>
preconnect links tell the browser to start the connection to Google's servers early — this improves loading speed.body {
font-family: 'Lato', sans-serif; /* fallback if font fails */
}
h1, h2, h3 {
font-family: 'Merriweather', Georgia, serif;
}
Self-hosted fonts with @font-face
/* 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.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.
/* 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. */
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.
.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 */
5 — font-style
The font-style property renders text in italic (slanted) or normal (upright) form. Most use cases involve just two values.
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; }
font-style: normal — upright text, the default.
font-style: italic — slanted 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.
/* 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 */
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.
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.
/* 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
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.
.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 */
9 — text-decoration and text-transform
text-decoration
Controls underlines, overlines, and strikethroughs. Most commonly used to remove the default underline from links.
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.
.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.
/* 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.
/* 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 */
}
1rem/1.7. Omitting any optional part (style, weight, line-height) leaves it at its default value.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
| Property | What it controls | Key values |
|---|---|---|
font-family | Typeface and fallback stack | Font names (quoted if spaces), generic: serif sans-serif monospace |
font-size | Text size | 1rem (prefer) · px · em · % |
font-weight | Thickness / boldness | 100–900 · normal (400) · bold (700) |
font-style | Upright vs italic | normal · italic · oblique |
font | Shorthand for the above four + line-height | [style] [weight] size[/lh] family |
line-height | Vertical space between lines | Unitless number (e.g. 1.6) — preferred |
letter-spacing | Space between characters | em values — positive opens, negative tightens |
word-spacing | Space between words | em or px |
text-align | Horizontal alignment | left · center · right · justify |
text-decoration | Underline, strikethrough, overline | none · underline · line-through · overline |
text-transform | Capitalisation | uppercase · lowercase · capitalize · none |
text-shadow | Shadow behind text | x 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.
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.h1, h2, h3, h4, h5, h6 { font-weight: 700; }, then write individual rules for each size.* { 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;
}
<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.text-decoration: none on the base a rule, then add it back on a:hover. Use text-underline-offset to control the gap.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;
}
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.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.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;
}
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.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.* { 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);
}