Ch. 5 · HTML & CSS

CSS Responsive Units and Content Constraints

When to use px, rem, em, %, ch, vw and dvh, how clamp() builds fluid type that still zooms, and why 100vh breaks on phones, with real numbers.

~6 min readintermediateupdated Oct 7, 2026

CSS has more length units than most people use, and interviews tend to probe the handful that matter: why rem instead of px for text, what goes wrong with viewport units, and how to make type scale with the screen without breaking zoom. Each unit answers a different question: relative to what should this size be? Picking the unit is really picking the thing it should follow.

Absolute and font-relative units

px is a CSS pixel, a fixed reference size (not a device pixel; a 3× phone draws one CSS pixel with nine device pixels). It’s right for things that shouldn’t scale with text: hairline borders, shadow offsets, small fixed gaps in icon layouts.

rem is relative to the root element’s font size. By default, browsers set that to 16px, so 1rem is 16px and 1.5rem is 24px. But users can change the default in their browser settings (Chrome’s “Font size” setting, Firefox’s “Default font size”). Someone with low vision might choose 20px or 24px. With font-size: 1rem, your body text follows their choice. With font-size: 16px, it doesn’t. That’s the main reason to use rem for type.

Note that browser zoom (Ctrl/Cmd and +) scales everything, including px, so px text does get bigger with zoom. It’s the default-font-size setting that px ignores. People confuse the two in interviews, and the distinction is worth stating precisely.

em is relative to the current element’s font size (or, when used for font-size itself, the parent’s). It compounds: a list with font-size: 0.875em nested three deep ends up at 0.875³ ≈ 0.67 of the outer size. That makes em awkward for type but handy for spacing that should scale with a component’s own text:

.button {
  font-size: 1rem;
  padding: 0.5em 1em;
}

.button.small {
  font-size: 0.875rem;
}
CSS

The small button’s padding shrinks with its text automatically (7px by 14px instead of 8px by 16px) because em follows the button’s font size.

ch is the width of the “0” glyph in the current font. It’s the natural unit for line length:

.article {
  max-width: 65ch;
}
CSS

That caps lines at around 65 characters, the range typographers recommend for comfortable reading, and it adjusts automatically if the font or font size changes. In a typical proportional font, 65ch is a bit wider than 65 average characters, since “0” is wider than many letters, but it’s close enough.

Percentages and constraints instead of fixed widths

% is relative to the containing block (for widths, its width). The point of percentages and max-width is to let content shrink. A common anti-pattern:

.card {
  width: 600px;
}
CSS

On a 375px phone, the card overflows by 225px plus padding. The constraint version:

.card {
  width: 100%;
  max-width: 600px;
}
CSS

or, combining both in one declaration, width: min(100%, 600px). On the phone it’s 375px (minus whatever padding the parent has); on a laptop it stops at 600px. The fixed 600px was never really the requirement. The requirement was “no wider than 600px”, and max-width says exactly that.

Viewport units and the mobile toolbar problem

vw and vh are 1% of the viewport’s width and height. 100vw is the full viewport width, which includes the vertical scrollbar on desktop, so width: 100vw on a page with a scrollbar overflows horizontally by the scrollbar’s width (often 15px on Windows). width: 100% on a block element avoids that.

On mobile, 100vh has a different problem. Mobile browsers show and hide their address bar and toolbar as you scroll. vh was defined as the largest possible viewport (toolbars hidden), so a height: 100vh hero is taller than the visible screen when the page loads with toolbars showing, and its bottom (often the call-to-action button) sits behind the toolbar.

The newer units name the viewport you mean:

  • svh: small viewport height, with toolbars shown. 100svh always fits on screen.
  • lvh: large viewport height, toolbars hidden. Same as the old vh.
  • dvh: dynamic, updates as toolbars move. Accurate, but changes size during scrolling, which can make content jump.

For a full-screen hero, min-height: 100svh is usually the right choice. All current browsers support these units; for older ones, put min-height: 100vh on the line before it as a fallback.

Fluid type with clamp()

clamp(min, preferred, max) returns the preferred value, kept between the minimum and maximum. For an article’s intro paragraph that grows a little with the screen:

.intro {
  font-size: clamp(1rem, 0.875rem + 0.5vw, 1.25rem);
}
CSS

The preferred value is 14px + 0.5vw (with the default 16px root). Evaluated at real widths:

viewport    0.875rem + 0.5vw    clamped result
320px       15.60px             16px   (minimum)
768px       17.84px             17.84px
1000px      19.00px             19px
1280px      20.40px             20px   (maximum)
Text

Why mix rem into the preferred value instead of just writing 5vw? Because vw doesn’t respond to zoom the way people expect. At 200% zoom, the CSS viewport of a 1280px window becomes 640px wide, so vw values halve in CSS pixels. Text set in pure vw is drawn at the same physical size: zoom does nothing, which fails WCAG 1.4.4 (Resize Text). With the mixed formula, the 1280px window at 200% zoom computes 14 + 3.2 = 17.2 CSS pixels, drawn at 34.4 device pixels instead of 20. That’s 1.72 times larger, not quite 2, which is why the vw part should stay small relative to the rem part, and why you should test at 200%. Keeping the maximum within about 2.5 times the minimum also helps.

Tools generate these formulas for a chosen size range, but you should be able to explain what each part does.

A layout, worked through

A blog post page combining all of this:

:root {
  font-size: 100%; /* respect the user's default, don't force 16px or 62.5% */
}

body {
  font-size: 1rem;
  line-height: 1.6;
  margin: 0;
}

.post {
  max-width: 65ch;
  margin-inline: auto;
  padding-inline: clamp(1rem, 4vw, 2rem);
}

.post h1 {
  font-size: clamp(1.75rem, 1.25rem + 2vw, 2.75rem);
  line-height: 1.15;
}

.hero {
  min-height: 100vh;
  min-height: 100svh;
}

hr {
  border: 0;
  border-top: 1px solid #e4e4e7;
}
CSS

line-height: 1.6 is unitless on purpose: unitless line heights are inherited as a ratio, so each element computes its own line height from its own font size. line-height: 1.6em would be computed once on body and inherited as a fixed length, so a 2.75rem heading would get body-text line spacing and its lines would overlap.

The old html { font-size: 62.5%; } trick (making 1rem equal 10px for easy maths) still respects user settings because it’s a percentage, but it forces you to set body text to 1.6rem everywhere, and third-party components that assume 1rem is 16px render tiny. Most teams now leave the root alone.

Interview practice

A marketing page sets h1 { font-size: 6vw; }, the body text to 14px, and the hero to height: 100vh. Users report that the heading doesn’t get bigger when they zoom, that text is too small for some, and that the hero’s button is hidden behind Safari’s toolbar on iPhones. Fix each issue and explain why.

Answer

The heading uses pure viewport units, so zooming shrinks the CSS viewport and 6vw shrinks in CSS pixels by the same factor; the drawn size stays constant. Replace it with a clamped mix, such as clamp(2rem, 1.5rem + 2.5vw, 3.5rem), so the rem part scales with zoom and the user’s font setting. The body text in px ignores users’ default font size setting, so someone who set 20px gets 14px anyway; use 1rem (or 0.875rem if the design truly needs it smaller, though 1rem is the better default for body copy). For the hero, 100vh is the large viewport on mobile, taller than the visible area when toolbars show; change it to min-height: 100svh with min-height: 100vh before it as a fallback, and use min-height instead of height so long content can still grow the section. Then test at 200% zoom and with a larger default font size, and on a real phone with toolbars visible.

Keep going

More in HTML & CSS

read ✓HTML & CSS · mid

CSS Container Queries for Reusable Components

Build a card that switches layout based on the space it's given, not the viewport: container-type, named containers, cqi units and the gotchas.

~6 min readread →
read ✓HTML & CSS · mid

CSS Grid Tracks and Minmax Sizing

How fr shares free space, what minmax() and auto mean for a track, and the math behind repeat(auto-fill, minmax(220px, 1fr)) at real widths.

~5 min readread →
esc