All posts

Type that scales: responsive font sizes without the jumps

On a tablet, the heading inside a card was almost as large as the section heading above it. Fixed pixels and two media queries were to blame. How we rebuilt our own site on rem and clamp(), with the measured numbers before and after.

The same heading at three device widths side by side, each time in the right proportion to the line above it

It rarely shows up while you are building. It shows up later, when somebody opens the site on a tablet. The heading above a card sits there almost as large as the section heading above it. On a phone, the small line above a heading suddenly reads almost level with it. On a desktop everything is fine.

That is exactly what we had on this site. Measured at 1440, 900 and 375 pixels of window width:

Element Desktop Tablet Phone
Hero heading 66 px 44 px 34 px
Section heading 46 px 38 px 30 px
Heading inside a card 32 px 32 px 26 px
Line above the section heading 17 px 17 px 17 px

The last row is where the mistake sits. The small line above a heading, on our site it reads "Client projects", was 17 pixels at every width. The heading next to it fell from 46 to 30. On a desktop one was therefore 2.7 times the size of the other, on a phone only 1.8 times. Half of the distance that tells a label and a heading apart was gone.

The third row is the same mistake in the other direction: the heading inside the card stayed at 32 pixels while the section heading above it dropped to 38. At 768 pixels of window width the two read almost level, even though one is subordinate to the other.

Why it happens

Two habits together produce it reliably.

Everything is in pixels. On our site that was 183 declarations, every single one in px, none in rem, none in clamp().

The size changes at media queries. In our case at two of them, 1100 and 720 pixels. Between the jumps, nothing happens.

The consequence is that every heading needs its steps individually: one for the desktop, one from 1100 down, one from 720 down. Miss one and you will not notice, because the page does not look broken. It only looks wrong at a width you do not happen to have open. In our case the steps were missing on the card and on the label, and those were exactly the two that showed.

What good practice looks like today

The short version, and it really is this short:

  1. rem as the base unit. That way type follows the browser setting.
  2. clamp() for anything large. That way it grows continuously with the window width instead of jumping.
  3. vw only as the middle term, never on its own. The floor and the ceiling are in rem.

The third point is the one most often got wrong, so it comes first.

Why pure vw does not work

font-size: 4vw sounds like the simplest answer to fluid typography. It is not accessible, though, and the reason is uncomfortably elegant:

When somebody zooms a page to 200 per cent, the CSS pixel gets bigger. A window that was 1400 CSS pixels wide is 700 wide afterwards. And 4vw of 700 is exactly half of 4vw of 1400.

The magnification and the shrinking cancel out precisely. After zooming, the text sits there at the same size as before. Anyone who zooms because they cannot see well has gained nothing. That breaks WCAG success criterion 1.4.4, which requires text to be resizable to 200 per cent without loss of content.

Inside clamp(), vw is harmless as long as the floor and ceiling are in rem: on zoom those two grow, and the type lands on the floor instead of in nothing.

Why px ignores the browser setting

The second point that easily slips past: font-size: 17px stays 17 pixels, even when somebody sets the font size to "large" or "very large" in their browser settings. That setting only changes the reference value for rem, and a pixel value never asks for it.

This is not a rare case. It is the setting people use before they discover zoom, and many leave it on permanently. On a site that calculates entirely in pixels it has no effect whatsoever.

The rebuild for it is small: no font size on html, so the browser default stands, and everything else divided by 16. 17 px becomes 1.0625rem.

clamp() in one line

font-size: clamp(2.75rem, 2.3333rem + 1.8519vw, 4rem);

Three values: floor, fluid part, ceiling. The browser takes the middle value and lets it go no lower than the first and no higher than the third.

The middle term is the equation of a straight line, and you can work it out rather than guess at it. If you want type to be min at 360 pixels of window width and max at 1440:

slope   = (max - min) / (1440 - 360)
vw part = slope * 100
rem part = (min - slope * 360) / 16

For 44 to 64 pixels that gives the line above. There are calculators online that will do it for you, but three lines are typed faster than a page is opened.

The fluid part always needs a rem component. A pure vw middle term collapses into the floor on zoom, and then you have the jumps back, just somewhere else.

A scale instead of a list

That settles the technique. The real problem was the order.

Our 183 declarations came down to 36 different values, and many of them were half a pixel apart: 17.5 and 17 and 16.5. No human ever decided that these three are three different things. They accumulated.

Out of that we made eleven steps, a geometric series with a ratio of 1.13. Each step is 1.13 times the next one down, on the phone as on the desktop. That is the point: when every step grows along the same curve, the ratio between two headings stays the same at every width, and the ranking holds from phone to desktop.

The side effect was the real surprise: no heading moved by more than 1.5 pixels on the desktop. The values were already close to a clean series, they had simply never been thought of as one. Everything that changed lies below desktop width.

The same table as above, now afterwards:

Element Desktop Tablet Phone
Hero heading 64 px 54 px 44.3 px
Section heading 44.5 px 38.5 px 32.7 px
Heading inside a card 31 px 27.7 px 24.6 px
Line above the section heading 17 px 16 px 15 px

The ratio of section heading to label is now 2.6 on the desktop and 2.2 on the phone instead of 1.8. And the card sits one step below the section heading again, at every width.

What must not scale

One thing that is missing from a lot of guides: body text does not belong in the fluid part.

It is tempting to push everything through the same formula. The result, though, is that text gets smaller on the phone, which is precisely where there is least room and the reading conditions are worst. Our reading sizes are therefore fixed, just in rem instead of px: a paragraph is the same size on a phone as on a desktop.

What is fluid, for us, is only the headings, the intro below them and the label above them. Everything else has a value.

Plus a second floor we did not have before: in the hero cards, type was set in cqw, that is relative to the width of the card itself. That is the right unit for it, but it has no limit either way. One of the tags came out at 10.5 pixels at full window width because of it. Container units are excellent, but just like vw they need a clamp() around them.

While we were at it: grey is not a colour for small caps

Something else surfaced during the measuring that has to do with size. The line under a post title, "6 Aug 2026 · 12 min read · by Ronny Arndt", was set in a grey with a contrast of 5.5 : 1. That meets the standard, which asks for 4.5 : 1.

It was still barely readable, and the reason is the combination: 12.5 pixels, all caps, wide tracking. Capitals consist almost entirely of thin verticals, and at a small size with light strokes there is too little colour left on the surface. A contrast figure measures two areas of colour against each other and says nothing about how much area a letter actually covers.

Three changes, each small on its own:

  • grey from 5.5 : 1 to 8.5 : 1
  • every micro size on the site down to one value instead of 12.5 / 13 / 13.5
  • small caps set semibold instead of light

The standard is a floor. Anyone who hits 4.5 : 1 and considers the matter closed builds sites that do not work on a bright screen in daylight.

How to check this yourself

Without measurement all of this is taste. Three routes, none of them costing anything:

  1. Set the developer tools to various widths and read off the computed font size rather than the one in the stylesheet. What matters is the widths between the devices: 900, 1024, 1180. That is where the mistakes are.
  2. Set the font size in your browser to "very large". If nothing changes, the site is in pixels.
  3. Zoom to 200 per cent and check that the headings come along. If they stand still, there is pure vw somewhere.

For us, the second and the third came up empty before the rebuild. That is the uncomfortable part: you do not see it unless you go looking.

If you want to know how your site holds up between device widths, drop us a line. We measure the headings at six widths and send you the table.

Sources

How current this post is

Reworked for language. The measured values in the post still hold.

Common questions about responsive font sizes

Are pixels forbidden for font sizes?

No, and that is a common misreading. Pixels are right in plenty of places: border widths, padding inside a control, anything that should not grow with the text. They are wrong wherever type is set. A value in px does not follow the browser font size setting. Someone who sets it to "very large" because they otherwise struggle to read will not see a single letter grow on a pure pixel site.

What is the difference between rem and em?

`rem` always refers to the root font size of the document, `em` to the font size of the element the value sits in. That makes `em` inherit and compound: a list at `font-size: 0.9em` inside a list at `font-size: 0.9em` ends up at 0.81. For font sizes, `rem` is the calmer choice. For spacing that should match the type of its own element, `em` is still right.

Why avoid font-size in vw?

Because the text is then no longer resizable. Zooming a page to 200 per cent halves the window width in CSS pixels. A value in pure vw shrinks by exactly as much as the zoom was meant to grow it, and the text ends up unchanged. That breaks WCAG success criterion 1.4.4. Inside clamp(), vw is harmless, because the floor and the ceiling are in rem and do take the zoom with them.

How many type sizes does a website need?

Fewer than accumulate on their own. Our site carried 183 declarations across 36 different values, many of them half a pixel apart. That became eleven steps for headings and eight reading sizes. The principle behind it: whoever adds a heading picks an existing step. An in-between value is always a sign that the ranking has not been thought through.

Over which widths should type scale?

We calculate from 360 to 1440 pixels of window width. Below that the floor applies, above it the ceiling. 360 is the width that virtually no device in use falls under, 1440 the width beyond which a heading should stop growing, or it breaks out of the reading column. The ceiling matters most: without it, type gets absurd on a 34-inch screen.