FullStack Course LogoFullStack Course
Module: CSS
CSS·022·10 MIN READ

022: Typography

TOPICS COVERED: Typography

Learning outcomes

By the end of this lesson, you can assemble a readable font stack, size text with relative units, and tune line height, measure, weight, alignment, and spacing. You will also be able to establish a clear type hierarchy, load web fonts without treating them as free performance, and test text at different zoom levels and viewport widths.

Prerequisites and retrieval

Bring forward the palette from 021. Before writing new CSS, retrieve which text properties inherit and why foreground and background contrast have to be considered together. Also read the portfolio with CSS disabled. Its heading structure should still communicate the document's meaning before visual hierarchy is added.

Terminology

  • Typeface/font family: The named visual design of text; font-family selects from available families. — Source: MDN: font-family
  • Font stack: An ordered list of fallback families in font-family, ending with a generic family. — Source: MDN: font-family
  • Generic family: Keyword categories such as serif, sans-serif, monospace, and system-ui. — Source: MDN: font-family
  • Root em (rem): A length unit relative to the root element's font size. — Source: CSS Values 4: rem
  • Em (em): Relative to the parent font size for font-size, and to the element's own font size for other properties. — Source: CSS Values 4: font-relative lengths
  • Line height/leading: The vertical space allocated to each line box through line-height. — Source: MDN: Styling text
  • Measure: The length of a line of text, commonly constrained with ch units. — Source: CSS Values 4: ch
  • Weight: Text stroke thickness on the 100–900 scale, where 400 is normal and 700 is bold. — Source: MDN: Fundamental text styling
  • Web font: A font file downloaded by the page through @font-face. — Source: MDN: Web fonts
  • Typography (formal): "Typography is the art of arranging type — font, size, line height, measure, and hierarchy — for legible reading." — Source: MDN: Styling text
  • Character unit (ch): "ch represents the width of the 0 glyph in the element's font." — Source: CSS Values and Units Level 4: ch
  • System font stack: "A font-family list starting with system-ui that resolves to the platform UI font." — Source: MDN: System fonts

Mental model: typography is a reading system

Typography is more than picking a decorative font. It coordinates family, size, line height, weight, width, spacing, and contrast so readers can scan the hierarchy and continue reading comfortably. Browser defaults also respect user preferences, while relative units help that adaptability survive your design decisions.

In the usual case, 1rem resolves to the root font size. Setting the root to 62.5% simply to make decimal pixel conversions look convenient makes the math less direct and can conflict with user expectations. Keep body text close to the user's default, then build hierarchy with modest ratios. A unitless line-height is inherited as a multiplier, so every descendant calculates its line spacing from its own font size rather than inheriting one frozen pixel value.

Long lines make readers work harder. max-inline-size: 65ch keeps a prose block near 65 zero-character widths while still allowing it to shrink on a narrow screen. Avoid fixed heights around text: zoom, translation, and fallback fonts can all change where the text wraps.

Beginner example: portfolio type hierarchy

css
body {
  margin: 0;
  color: rgb(30 41 59);
  background: rgb(248 250 252);
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: 1rem;
  line-height: 1.6;
}

h1,
h2,
h3 {
  color: rgb(15 23 42);
  line-height: 1.2;
}

h1 {
  margin-block: 0 0.5rem;
  font-size: 2.5rem;
  letter-spacing: -0.025em;
}

h2 {
  margin-block: 2rem 0.75rem;
  font-size: 1.75rem;
}

h3 {
  margin-block: 0 0.5rem;
  font-size: 1.25rem;
}

p,
li {
  max-inline-size: 65ch;
}

.intro {
  font-size: 1.125rem;
  color: rgb(71 85 105);
}

.project-meta {
  font-size: 0.875rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

Add HTML:

html
<header>
  <h1>Asha Rao</h1>
  <p class="intro">Frontend developer focused on inclusive interfaces.</p>
</header>
<main>
  <section aria-labelledby="work-title">
    <h2 id="work-title">Selected work</h2>
    <article class="project">
      <p class="project-meta">HTML · Accessibility · 2026</p>
      <h3>Library finder</h3>
      <p>A clearer route from location search to opening hours.</p>
    </article>
  </section>
</main>

The HTML level, not the font size, carries heading meaning. CSS makes those levels look different, but it cannot fix a heading structure that is wrong in the markup. Body text gets a generous line height; headings use a tighter value because large display text needs less relative leading. Slightly negative tracking can work for a large system-font heading, while uppercase metadata usually needs extra spacing. Wide tracking on a long paragraph makes sustained reading harder.

At 200% zoom, text should grow, wrap, and remain available. If a box clips it, investigate the box's fixed dimensions rather than treating the text as the problem.

Intermediate example: readable article content

html
<article class="case-study">
  <header>
    <p class="case-study__kicker">Case study</p>
    <h1>Making library search understandable</h1>
    <p class="case-study__summary">A semantic redesign tested with keyboard navigation and browser zoom.</p>
  </header>
  <h2>The problem</h2>
  <p>Visitors needed opening hours, but labels and link text were ambiguous.</p>
  <blockquote>
    <p>Clear words reduced hesitation more than visual decoration.</p>
  </blockquote>
</article>
css
.case-study {
  max-inline-size: 70ch;
  margin-inline: auto;
  padding-inline: 1rem;
}

.case-study__kicker {
  margin-block-end: 0.25rem;
  color: rgb(29 78 216);
  font-weight: 750;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.case-study__summary {
  font-size: 1.25rem;
  line-height: 1.5;
}

.case-study blockquote {
  margin-inline: 0;
  padding-inline-start: 1rem;
  border-inline-start: 0.25rem solid rgb(37 99 235);
  color: rgb(51 65 85);
  font-size: 1.125rem;
}

.case-study a {
  color: rgb(29 78 216);
  text-decoration-thickness: 0.1em;
  text-underline-offset: 0.18em;
}

margin-inline: auto centers a block when its inline size is constrained, while the content can still fill a narrow viewport. Logical properties also keep the rule meaningful in different writing directions. Avoid justified body text on the web: especially in narrow columns, its uneven spacing can create distracting rivers through the paragraph.

Optional advanced example: responsible web fonts

System fonts are fast and familiar. Use a local web font only when the design has a real reason to download one:

css
@font-face {
  font-family: "Portfolio Sans";
  src: url("fonts/portfolio-sans.woff2") format("woff2");
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
}

body {
  font-family: "Portfolio Sans", system-ui, sans-serif;
}

WOFF2 is an efficient format for web delivery. A variable font can cover a weight range, represented here by 400 800, but only if the file actually supports that range. font-display: swap keeps fallback text visible while the custom face loads. Check that the license permits web embedding. Limit families, styles, scripts, and weights to what the page uses, then test for layout shift caused by different fallback metrics.

Mistakes, debugging, and DevTools

  • Missing generic fallback: if the preferred font fails, the rendering result becomes unpredictable.
  • Quoting incorrectly: multi-word family names require quotes; generic names do not.
  • Setting body text in pixels and assuming users cannot override it: use relative sizing and test browser settings.
  • Using line-height: 20px: descendants with larger type may collide; prefer unitless values.
  • Centering long paragraphs, using full-uppercase prose, or adding excessive letter spacing: each reduces sustained readability.
  • Restricting text with fixed width or height: content can clip under zoom or translation.
  • Faking hierarchy with font size while HTML headings are out of order: CSS cannot repair document semantics.
  • Loading many font files: text rendering slows and page weight grows quickly.

When typography looks wrong, inspect the text in DevTools instead of guessing. Computed styles reveal the final family, size, line height, and inherited sources. Many browsers also expose a Fonts panel showing which font actually rendered; a family named in CSS may not be available. In Network, filter by “Font” and inspect status, transfer size, timing, and CORS errors. Disable the custom face to see whether the fallback is the source of the problem.

Accessibility and performance

WCAG text-resize guidance requires content to remain usable when text is resized to 200% without assistive technology. Reflow guidance expects content at narrow equivalent widths without two-dimensional scrolling, except for content that is genuinely two-dimensional. Do not disable user zoom in the viewport meta tag.

For paragraphs, a line height around 1.5 is a useful starting point, not an absolute law. WCAG 1.4.12 also requires that content remain usable when users override spacing to at least 1.5 times line height, 2 times the paragraph font size, 0.12 times letter spacing, and 0.16 times word spacing. Flexible boxes and avoiding fixed text heights make those overrides much safer.

Fonts can delay rendering and shift layout. Prefer system fonts when branding does not justify a download. Otherwise use WOFF2, only the subsets and weights you need, appropriate caching, and visible fallback behavior. Never turn essential text into an image.

Deep dive: every major text control

A typography system is easier to reason about when each property has a defined job:

css
.article {
  font-family: ui-serif, Georgia, serif;
  font-size: 1rem;
  font-weight: 400;
  font-style: normal;
  line-height: 1.7;
  letter-spacing: 0;
  word-spacing: normal;
  text-align: start;
  text-decoration: none;
  text-transform: none;
}

font-family chooses the typeface stack. font-size sets the em size, font-weight selects the weight, and line-height controls line-box height. Spacing and decoration properties should address a real reading or interface need, not reproduce arbitrary values from a design screenshot.

Font shorthand

The font shorthand can set several font properties together:

css
.card-title {
  font: 700 1.25rem/1.25 system-ui, sans-serif;
}

It is compact, but omitted subproperties may be reset. While learning, or when a partial change needs to be obvious, longhands are usually easier to inspect.

Deep dive: web-font loading

A local font file is declared with @font-face:

css
@font-face {
  font-family: "InterCourse";
  src: url("/fonts/inter-var.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

body {
  font-family: "InterCourse", system-ui, sans-serif;
}

Important considerations:

  • Prefer WOFF2 for modern web delivery.
  • Subset font files when the project controls the font pipeline.
  • Avoid loading many families and weights without a clear need.
  • font-display: swap allows fallback text to render instead of remaining invisible while the font downloads.
  • Test layout shift because fallback and web fonts can have different metrics.

A hosted provider such as Google Fonts can simplify setup, while local or self-hosted fonts provide more control over privacy, caching, and request policy.

Worked example: responsive type without breakpoint piles

css
.hero-title {
  font-size: clamp(2rem, 1.35rem + 3vw, 4.5rem);
  line-height: 1.02;
  letter-spacing: -0.025em;
  max-inline-size: 14ch;
}

.hero-copy {
  font-size: clamp(1rem, 0.95rem + 0.35vw, 1.25rem);
  line-height: 1.6;
  max-inline-size: 60ch;
}

clamp(minimum, preferred, maximum) gives the browser a fluid range while keeping both extremes readable. The preferred expression should not depend so heavily on viewport width that user zoom loses its effect. Test the result at zoom, not only at the default viewport size.

css
.article a {
  color: #1d4ed8;
  text-decoration-line: underline;
  text-decoration-thickness: 0.12em;
  text-underline-offset: 0.18em;
}

.article a:hover {
  text-decoration-thickness: 0.2em;
}

.article a:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: 3px;
}

The underline gives links a distinction that remains available even when color perception is reduced.

Deep dive: measure, rhythm, and wrapping

Readable long-form text usually benefits from a bounded line measure:

css
.prose {
  max-inline-size: 68ch;
  margin-inline: auto;
}

ch approximates the width of the 0 glyph. It is useful for text measure, but it is not an exact pixel-geometry unit.

When supported, balanced wrapping can help headings:

css
h1,
h2 {
  text-wrap: balance;
}

Paragraphs may use:

css
.prose p {
  text-wrap: pretty;
}

These properties refine wrapping; they do not replace sensible width constraints.

Deep dive: text transformations and shadows

css
.eyebrow {
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

Do not put essential text in the wrong case merely to compensate with CSS. Screen readers and copy-and-paste users should receive meaningful source text.

Text shadows can support a decorative headline, but readability should not depend on them:

css
.hero-title {
  text-shadow: 0 2px 12px rgb(0 0 0 / 0.35);
}

If a photograph requires a heavy shadow before text can be read, fix the contrast with a scrim, an overlay, or a different image crop.

Typography stress test

Test each page with:

  • browser zoom at 200%;
  • text-only zoom where available;
  • a very long heading;
  • a long unbroken URL or identifier;
  • font loading disabled or throttled;
  • a system font fallback;
  • narrow viewport;
  • high-contrast or forced-color mode where available.

A typography system earns its keep under stress. It should remain readable when the preferred font is slow, unavailable, or surrounded by unusually long content, not only when a short mockup renders at the default size.

Font variants and numeric UI

css
.invoice-total,
.metric {
  font-variant-numeric: tabular-nums;
}

Tabular numerals give digits equal advance widths, which can keep changing financial or dashboard values aligned.

Other font-variant-* features support ligatures, caps, East Asian text, and more. Use them only when the chosen font supports the feature and the content benefits from it.

Hosted fonts versus self-hosting

A provider such as Google Fonts may be included with stylesheet links, whereas self-hosting uses your own @font-face files. Compare:

  • privacy and policy requirements;
  • cache behavior;
  • number of external connections;
  • control over font files and subsets;
  • operational simplicity.

Do not ship ten font weights when the design uses only 400 and 700.

Tiered exercises

Checkpoint: read under stress

Replace one short paragraph with several sentences, enlarge the browser's default font, and narrow the window. Scan the heading, summary, metadata, body, and link in order. The hierarchy should remain apparent without relying on color alone, and no line should vanish behind a fixed box. This stress test tells you more than a perfectly short mockup does.

Next, disable the preferred font in DevTools. Check whether fallback metrics produce clipped labels, overlapping controls, or an unacceptable layout shift. A font stack is a sequence of real rendering outcomes, not decorative documentation. If the fallback is substantially wider, flexible containers and wrapping should absorb the difference. Keep actual bold and italic faces available when those distinctions carry meaning.

Foundation: Set a system font stack and unitless body line height. Create distinct h1, h2, and h3 sizes in rem without changing HTML order.

Core: Limit prose measure, style intro and metadata text, and verify the portfolio at 200% zoom and in a narrow window.

Stretch: Add one licensed WOFF2 font with a system fallback and font-display: swap. Compare Network transfer and fallback behavior; remove it if the design improvement does not justify the cost.

css
body {
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: 1rem;
  line-height: 1.6;
}
h1, h2, h3 { line-height: 1.2; color: rgb(15 23 42); }
h1 { font-size: 2.5rem; letter-spacing: -0.025em; }
h2 { font-size: 1.75rem; }
h3 { font-size: 1.25rem; }
p, li { max-inline-size: 65ch; }
.intro { font-size: 1.125rem; color: rgb(71 85 105); }
.project-meta {
  font-size: 0.875rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.case-study { max-inline-size: 70ch; margin-inline: auto; padding-inline: 1rem; }
a { text-underline-offset: 0.18em; }
a:focus-visible { outline: 3px solid rgb(249 115 22); outline-offset: 3px; }

Recap and exit questions

Typography is an adaptive reading system. Relative sizes, unitless line height, restrained measure, reliable fallbacks, and semantic headings usually provide more value than a decorative font choice.

  1. How do rem and em differ?
  2. Why is unitless line height useful?
  3. What problem does max-inline-size: 65ch address?
  4. Why can a loaded font still render poorly while it arrives?
  5. What tests should pass at 200% zoom?

Official references

Reader page: /css/lesson/022/typography