Skip to main content
    Courses/HTML & CSS/Responsive Typography

    Lesson 20 • Advanced Track

    Responsive Typography with clamp()

    By the end of this lesson you'll be able to make text that scales smoothly from phone to desktop with a single line of CSS, build a balanced type scale, set a comfortable line length, and do it all without breaking the zoom your low-vision readers rely on.

    What You'll Learn

    Choose between rem, em and px and know why rem wins for font sizes
    Write fluid type with clamp(min, preferred-vw, max) — no media queries
    Build a modular type scale so heading sizes feel deliberate
    Set readable line-height and cap line length (measure) with ch
    Tidy ragged headings and paragraphs with text-wrap: balance / pretty
    Keep all of it accessible so browser zoom still enlarges your text
    Before this lesson: you should be comfortable writing CSS rules, setting font-size and line-height, and you'll get more out of it if you've met design tokens in CSS Custom Properties. No maths background needed — every formula here is explained in plain English.

    💡 Think of It Like This

    clamp() is a thermostat with a guaranteed range. You set a minimum the room can never fall below, a preferred setting that drifts with the weather outside (your viewport width), and a maximum it can never exceed. The temperature glides between those bounds on its own — you never have to step in for "if it's between 18° and 22°, do this."

    Old-school responsive type was the opposite: a stack of if statements (media queries) that snapped the font from one fixed size to the next at hard breakpoints — comfortable at each chosen width, jumpy in between. clamp() replaces the staircase with a ramp. One line gives you a smooth, bounded size at every width.

    1. rem vs em vs px — Pick the Right Unit

    Before you can size text responsively, you need to size it accessibly, and that comes down to the unit. A px is a fixed device pixel — it never changes. A rem is relative to the root font size (the <html> element), so 1rem follows whatever base size the user picked. An em is relative to the current element's font size, so it tracks its immediate context — and compounds when you nest things.

    UnitRelative toBest forZoom-safe?
    remRoot (html) font sizeFont sizes, spacing, layout✅ Yes
    emThis element's font sizePadding/gaps that track their text✅ Yes (but compounds)
    pxNothing — fixedBorders, shadows, hairlines⚠️ Ignores font preference
    vw1% of viewport widthFluid growth (inside clamp!)❌ Ignores zoom on its own

    Run the worked example and watch the trap spring: the em heading nested inside another em heading gets huge, because each em multiplies the one above it.

    Worked example: how each unit responds

    rem follows the root, em compounds, px stays put

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Units compared</title>
      <style>
        /* The root font size. Change 16px to 24px and watch every rem grow,
           every px stay frozen — that is exactly what a user's "larger text"
           browser setting does. */
        html { font-size: 16px; }
    
        body { background:#0f172a; color:#e5e7eb; font-family: system-ui, sans-serif; padding:24px; }
        .row { b
    ...

    2. Fluid Type with clamp(min, preferred-vw, max)

    clamp() takes three values and returns the middle one — unless it strays outside the bounds, in which case it's pinned to the nearest one. So clamp(2rem, 5vw, 4rem) means: aim for 5vw, but never smaller than 2rem and never larger than 4rem. It is literally max(min, min(preferred, max)), just readable.

    The pattern that keeps it accessible: a rem floor, a vw-based middle so it grows with the screen, and a rem ceiling. Because the floor and ceiling are in rem, browser zoom still enlarges the text — the thing a bare vw can't do. Resize the preview to see it glide.

    Worked example: a fluid clamp() heading scale

    Resize the preview and watch every heading scale smoothly

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Fluid type scale</title>
      <style>
        * { box-sizing: border-box; margin:0; padding:0; }
        body { background:#0f172a; color:#e5e7eb; font-family: system-ui, sans-serif; padding:30px; }
    
        /* FLUID TYPE SCALE — one clamp() per level, no media queries.
           Pattern is always clamp(rem-floor, vw-growth, rem-ceiling). */
        h1 { font-size: clamp(2
    ...

    3. A Modular Type Scale

    Picking font sizes at random gives a page that feels off. A modular scale picks one base size and one ratio, then multiplies up: each step is the previous size times the ratio. A ratio of 1.25 (the "major third") is a calm, common choice — 1rem, 1.25rem, 1.563rem, 1.953rem, and so on.

    Store the steps as custom properties so the scale lives in one place and every heading just reads a token. Wrap each one in clamp() and you get a scale that is both consistent (the ratio) and fluid (the clamp).

    Worked example: tokens built from one ratio

    One base size and ratio drive the whole heading scale

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Modular scale</title>
      <style>
        :root {
          /* Base 1rem, ratio 1.25 (major third). Each step = previous x 1.25,
             and each is clamped so it stays fluid AND readable. */
          --step-0: clamp(1rem, 0.95rem + 0.3vw, 1.125rem);   /* body */
          --step-1: clamp(1.25rem, 1.1rem + 0.8vw, 1.563rem); /* h3 */
          --step-2: clamp(1.563rem, 1.
    ...

    4. Line-height, Measure (ch) & text-wrap

    Size is only half of readable text. Line-height is the vertical space between lines — set it unitless (e.g. line-height: 1.6) so it multiplies the font size and scales with it; a px line-height won't. Measure is the line length, and the comfortable range is about 45–75 characters. Cap it with max-width: 65ch, where 1ch is the width of a "0" in your font.

    Finally, text-wrap: balance evens out the lines of a heading so you don't get one orphan word on its own line, while text-wrap: pretty tidies the last lines of long paragraphs. Use balance for headings, pretty for body text.

    Worked example: measure, line-height and wrapping

    Cap the line length and balance a ragged heading

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Measure & wrapping</title>
      <style>
        body { background:#0f172a; color:#e5e7eb; font-family: system-ui, sans-serif; padding:30px; }
    
        h1 {
          font-size: clamp(1.75rem, 4vw, 2.75rem);
          color:#3b82f6;
          max-width: 22ch;          /* short measure for the headline */
          text-wrap: balance;       /* even out the lines, no lonely last w
    ...

    5. Respecting User Zoom

    People with low vision raise their browser's base font size or zoom the page, and accessibility guidelines require text to stay usable when zoomed to 200%. The golden rule: every font size must contain at least one rem (or em). A pure vw size ignores zoom entirely; a clamp() with rem bounds keeps growing.

    Keep body text at a minimum of 1rem (16px), never pin the root size in a way that fights the user, and your fluid type stays friendly to everyone. The worked example puts a zoom-safe and a zoom-hostile heading side by side.

    Worked example: zoom-safe vs zoom-hostile

    One heading grows with zoom; the other refuses to

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Zoom & type</title>
      <style>
        body { background:#0f172a; color:#e5e7eb; font-family: system-ui, sans-serif; padding:30px; }
        .card { background:#1e293b; padding:18px; border-radius:8px; margin:12px 0; }
        .tag { color:#3b82f6; font-weight:600; font-size:12px; text-transform:uppercase; }
    
        /* GOOD: rem floor + rem ceiling. Browser zoom and 
    ...

    🎯 Your Turn #1 — Make a heading fluid

    The heading below is frozen at one px size and the paragraph runs the full width. Convert the heading to a fluid clamp() and cap the paragraph's measure. Fill in the blanks marked ___, then run it and check the comments.

    Your Turn #1: clamp() a heading + cap the measure

    Replace a fixed px size with a fluid, accessible clamp()

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Your Turn 1</title>
      <style>
        /* 🎯 YOUR TURN — fill in the blanks marked ___ */
    
        body { background:#0f172a; color:#e5e7eb; font-family: system-ui, sans-serif; padding:30px; }
    
        h1 {
          /* 1) Make this fluid: min 1.75rem, grow at 5vw, max 3.5rem. */
          font-size: ___;       /* 👉 replace ___ with  clamp(1.75rem, 5vw, 3.5rem) */
          
    ...

    🎯 Your Turn #2 — Tokenise a scale and tidy the wrapping

    Add the two missing scale tokens, point the headings at them, and tidy the ragged wrapping with text-wrap. Use the right keyword for each: balance for the heading, pretty for the paragraph.

    Your Turn #2: scale tokens + text-wrap

    Fill in the type scale and choose the right wrapping

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Your Turn 2</title>
      <style>
        /* 🎯 YOUR TURN — fill in the blanks marked ___ */
    
        :root {
          /* 1) Add the two larger steps (ratio 1.25 from --step-1). */
          --step-1: clamp(1.25rem, 1.1rem + 0.8vw, 1.563rem);
          ___   /* 👉 add:  --step-2: clamp(1.563rem, 1.3rem + 1.4vw, 1.953rem); */
          ___   /* 👉 add:  --step-3: clamp(1.953rem,
    ...

    🧩 Mini-Challenge — A responsive article header from scratch

    Support is faded now — only an outline is given. Build a small article header that is fluid, consistently scaled, comfortably measured, and zoom-safe. Lean on the worked examples in sections 2–4 if you get stuck.

    Mini-Challenge: responsive typography from scratch

    Fluid scale + measure + wrapping, no hand-holding

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Mini-Challenge</title>
      <style>
        /* 🧩 MINI-CHALLENGE: a responsive article header
           1. In :root, declare a small type scale as tokens (ratio your choice):
                --title, --lead, --body  — each wrapped in clamp(rem, vw, rem)
           2. Style <h1> with var(--title), a short measure (~22ch) and
                text-wrap: balance
           3. Styl
    ...

    ⚠️ Common Errors (and the fix)

    • px font sizes break zoom. Setting body text in px (e.g. font-size: 18px) ignores a user's chosen base size, so "larger text" settings do nothing. Fix: size text in remfont-size: 1.125rem — and reserve px for borders and hairlines.
    • vw with no min/max. font-size: 5vw turns microscopic on a phone, enormous on a monitor, and ignores zoom entirely. Fix: wrap it — clamp(1rem, 5vw, 3rem) — so the rem floor and ceiling keep it readable and zoom-friendly.
    • Lines too long (no measure cap). A paragraph with no max-width runs edge-to-edge on a wide screen; past ~75 characters the eye struggles to find the next line. Fix: max-width: 65ch on your text containers.
    • Line-height in px. line-height: 24px doesn't scale when the font grows, so big headings cramp. Fix: use a unitless multiplier — line-height: 1.6.
    • balance on a long paragraph. text-wrap: balance is for short text and browsers cap how many lines it touches, so it won't help body copy. Fix: use balance on headings and pretty on paragraphs.

    📋 Quick Reference

    GoalUse this
    Accessible font sizefont-size: 1.125rem;
    Fluid heading (no media query)font-size: clamp(2rem, 6vw, 4rem);
    Floor / ceiling onlymax(1rem, 4vw) / min(4vw, 3rem)
    Type scale token--step-2: clamp(1.5rem, 1.3rem + 1.4vw, 1.95rem);
    Readable line heightline-height: 1.6; (unitless)
    Cap line length (measure)max-width: 65ch;
    Balance a headingtext-wrap: balance;
    Tidy a paragraph's last linestext-wrap: pretty;

    ❓ Frequently Asked Questions

    Should I use rem, em, or px for font sizes?

    Use rem for font sizes almost every time. A rem is always relative to the root (the <html> element), so 1rem is whatever the user's chosen base size is — usually 16px, but larger if they've turned font size up in their browser settings. That means rem text respects the user's preference. em is relative to the element's own font size, which is great for spacing that should track the text it sits next to (like padding inside a button) but surprising for font sizes because it compounds when nested. px is a fixed device pixel: fine for hairline borders and shadows, but it ignores the user's font preference, so never set body font size in px.

    Why is font-size: 5vw a bad idea on its own?

    A raw vw value is a pure percentage of the viewport width with no floor and no ceiling. On a 320px phone, 5vw is only 16px; on a tiny watch it becomes unreadable, and on a 2560px monitor it balloons to 128px. Worse, vw does not respond to browser zoom or the user's font-size preference at all, so a low-vision user who zooms in gets no larger text. The fix is to wrap it: clamp(1rem, 5vw + 0.5rem, 4rem). The rem-based min and max give you a readable floor and a sane ceiling, and because rem units honour zoom, the clamped value still grows when the user zooms.

    What does clamp(min, preferred, max) actually compute?

    clamp() returns the preferred (middle) value, but never lets it drop below min or rise above max. So clamp(1.5rem, 4vw, 3rem) says: try 4vw; if that comes out under 1.5rem use 1.5rem instead; if it comes out over 3rem use 3rem. It is exactly max(min, min(preferred, max)) written more readably. Put your accessible floor in the first slot, the fluid viewport-based growth in the middle, and your upper limit in the third — and you get smooth scaling between two breakpoints with no media queries.

    What is the ch unit and why limit line length with it?

    1ch is the width of the digit zero in the current font, so it's a rough stand-in for one character. Typographers find lines of roughly 45–75 characters the most comfortable to read; longer and the eye loses its place returning to the next line. Setting max-width: 65ch on your text containers caps the measure (line length) at about 65 characters regardless of how wide the screen is, which keeps long paragraphs readable on big monitors. Because ch is font-relative, the cap scales sensibly when the font size changes.

    What does text-wrap: balance do, and when should I use pretty instead?

    text-wrap: balance evens out the number of characters across every line of a block, so a two- or three-line heading doesn't end with one lonely word on the last line. It's meant for short text — headings, titles, pull quotes — and browsers only balance a handful of lines for performance. For long body paragraphs use text-wrap: pretty instead: it doesn't balance the whole block but it prevents orphans (a single short word stranded on the final line) and improves the last few lines. Rule of thumb: balance for headings, pretty for paragraphs.

    Does using clamp() and vw break accessibility or zoom?

    It can if you do it carelessly, but done right it's fine. The danger is a font size built only from viewport units, because vw ignores zoom. As long as your clamp() min and max are written in rem (or em), the text still grows when a user zooms or raises their base font size — WCAG requires text to be resizable up to 200% without loss of content. Keep body text at a minimum of 1rem, never set the root font size in a way that locks it (avoid html { font-size: 62.5% } pinned in px), and your fluid type stays accessible.

    🎉 Lesson Complete

    You can now build typography that's fluid, consistent and accessible. The essentials:

    • ✅ Size text in rem so it respects the user's font preference; keep px for borders
    • ✅ Make it fluid with clamp(rem-floor, vw-growth, rem-ceiling) — no media queries
    • ✅ Pick one base size and ratio for a modular type scale that feels deliberate
    • ✅ Use unitless line-height and cap the measure at ~65ch
    • text-wrap: balance for headings, pretty for paragraphs
    • ✅ Every font size keeps a rem in it so browser zoom still works

    Next up: Flexbox Advanced Patterns, where you'll lay out these beautifully-sized elements into flexible, responsive components.

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service

    Install LearnCodingFast

    Learn faster with the app on your home screen.