Patterns

Six patterns that come up in almost every stylesheet, with the unit each one rests on and the case where it breaks.

Fluid type with clamp()

A size that grows between two window widths is a straight line, written as a rem term plus a vw term inside clamp(). The rem term is what keeps the size tied to the reader's setting. The vw term alone would ignore zoom.

Zoom works against the vw term. At 200% the window is half as wide in CSS pixels, so the vw part shrinks and the text grows by less than twice: the heading below, clamp(1.5rem, 1rem + 2.5vw, 3rem), came out 1.33 times larger. A common rule of thumb keeps the maximum within 2.5 times the minimum. It is not a requirement of the standard. The clamp builder on the math page computes the line for two sizes and two widths and shows the size at 200% zoom.

One pitfall: when the minimum is larger than the maximum, clamp() uses the minimum and ignores the rest.

A heading from 1.5rem at 320px to 3rem at 1280px
h1 { font-size: clamp(1.5rem, 1rem + 2.5vw, 3rem); }

See also Math functions and the clamp builder

Spacing that follows the text

Use em for the insides of a component, such as the padding of a button, so the whole thing scales when its text does. Use rem for page-level rhythm that should follow the reader's setting and not the size of one component.

For vertical rhythm, lh is the height of a line of the element and rlh the height of a line of the root. margin-block: 1lh is exactly one blank line in whatever font the paragraph uses. A caution: a tall inline box stretches its line, so 3lh is not always three rendered lines.

One blank line between paragraphs, whatever the font
p + p { margin-block-start: 1lh; }

Breakpoints in em

A breakpoint in em follows the reader's default font size, so a reader who raised it gets the narrow layout earlier, which is the layout that fits larger text. So 40em changes the layout at 640px for most readers and at 960px for one whose default is 24px.

In a media query em and rem both mean the browser's default, never the size you set on html. Do not rely on a root size in CSS to move a breakpoint.

A two-column layout from 40em
@media (min-width: 40em) {
  .layout { display: grid; grid-template-columns: 1fr 2fr; }
}

Full-height sections on phones

100vh is defined as the large viewport, the window with the browser toolbars pulled away. Where toolbars retract, a block of that height reaches under them while they show. svh is the height with toolbars shown, lvh with toolbars away and dvh follows them as they move.

The three agree in a desktop window, and that is all this site has measured. It has not watched a phone, so the toolbar behaviour on this page comes from the definitions and not from a measurement. Write a vh line first, so a browser that does not know the newer unit keeps it, and the unit you want after it.

Fill the visible area, with a fallback
.opening {
  min-block-size: 100vh;
  min-block-size: 100svh;
}

Type that follows the box it sits in

Give the parent container-type: inline-size and size the component's insides with cqi. A heading at clamp(1.25rem, 7cqi, 2.5rem) follows the box it was placed in and ignores the window: 28px in a container 400px wide, stopped at 40px in a wide one.

The container is always an ancestor, never the element itself. Where no ancestor has container-type, cqi reads the small viewport, and the only sign is a heading that follows the window instead of its column. The fallback outside the @container block is there for that case too.

A heading that scales with its parent, with a plain fallback
.widget-slot { container-type: inline-size; }
.widget h2 { font-size: 1.25rem; }
@container (inline-size >= 0px) {
  .widget h2 { font-size: clamp(1.25rem, 7cqi, 2.5rem); }
}

Line length in ch

max-inline-size: 65ch caps a line at 65 zeros of the font in use, not at 65 characters. Ordinary prose is narrower than zeros: in IBM Plex Sans a line 65ch wide held about 87 characters of English text. Choose the number for the font, and leave the unit as it is.

The width of a ch depends on the font, so the same value gives different lines in different families. Use rch when two blocks in different fonts must line up.

A readable measure
.story { max-inline-size: 65ch; }