CSS & Front-End

Type That Fits Every Screen Without a Single Media Query

One clamp() per level replaces a wall of media queries, but only if the middle term is solved rather than guessed. Here is how the slope and the intercept fall out of two viewport widths and two sizes, why a bare vw value fails the moment a reader zooms, and which three numbers your theme already knows.

Type That Fits Every Screen Without a Single Media Query

Your h1 is set to 3rem. On a 27 inch monitor that reads as a headline. On a 360 pixel phone it wraps to four lines and shoves the first paragraph off the screen. The usual fix is a media query at 768 pixels that drops it to 2rem, and now the heading has two sizes and nothing in between: at 767 pixels it is small, at 769 it is large, and every width in the gap gets the wrong one of the two.

Then there are the other seven levels. h2 to h6 were chosen to sit under the old h1, the body copy under those, the small print under the images below that. Eight levels across three breakpoints is twenty four declarations that all have to stay in proportion with each other, by hand, forever.

One clamp() per level replaces the lot. The part that goes wrong is the middle argument. Most examples put a round vw number there and let the two ends land wherever they land, which is why so many fluid scales hit their ceiling at the wrong width, or stop responding when a reader zooms. The middle term is a straight line, and a line through two known points has exactly one answer.

What clamp() actually returns

clamp() takes three lengths and returns the middle one, held between the other two. It is defined as max(MIN, min(VAL, MAX)), worth reading twice: the lower bound is applied last, so if the bounds go in the wrong way round and MIN comes out larger than MAX, MIN wins and MAX is ignored. No error, no warning, just a size that never moves.

The two bounds are the easy part: the size you want on the narrowest screen you care about, and the size you want on the widest. The middle argument is the only one that has to know anything about the viewport, and the only one anybody gets wrong.

The calculator below solves all eight levels at once and prints the finished declarations, with the size at each end in px, rem and pt to check against a design. It runs entirely in your browser: nothing is uploaded and no request goes anywhere.

Fluid type scale calculator

Build a fluid type scale and get the finished clamp() for every level, with the middle term solved as a real slope and intercept rather than guessed at. Everything is worked out in this browser tab, nothing is uploaded and nothing is fetched.

The scale
Level At 320 px At 1280 px font-size Copy
h1 47.78 px2.986 rem · 35.83 pt 61.04 px3.815 rem · 45.78 pt clamp(2.986rem, 2.71rem + 1.381vw, 3.815rem)
h2 39.81 px2.488 rem · 29.86 pt 48.83 px3.052 rem · 36.62 pt clamp(2.488rem, 2.301rem + 0.939vw, 3.052rem)
h3 33.18 px2.074 rem · 24.88 pt 39.06 px2.441 rem · 29.3 pt clamp(2.074rem, 1.951rem + 0.613vw, 2.441rem)
h4 27.65 px1.728 rem · 20.74 pt 31.25 px1.953 rem · 23.44 pt clamp(1.728rem, 1.653rem + 0.375vw, 1.953rem)
h5 23.04 px1.44 rem · 17.28 pt 25 px1.562 rem · 18.75 pt clamp(1.44rem, 1.399rem + 0.204vw, 1.562rem)
h6 19.2 px1.2 rem · 14.4 pt 20 px1.25 rem · 15 pt clamp(1.2rem, 1.183rem + 0.083vw, 1.25rem)
body 16 px1 rem · 12 pt 16 px1 rem · 12 pt 1rem
small 13.33 px0.833 rem · 10 pt 12.8 px0.8 rem · 9.6 pt clamp(0.8rem, 0.844rem - 0.056vw, 0.833rem)

small falls to 12.8 px. Below 16 px, iOS Safari zooms the page the moment a visitor focuses a form control set at that size, and it is already at the edge of comfortable reading.

The middle term is the straight line through the two end points: the slope is the size difference divided by the viewport difference, written in vw, and the intercept is what is left of it at zero width, written in rem. rem is read against a 16 px root, the browser default. A larger ratio at the large end means more contrast there, so headings grow faster and small text shrinks further. Body has no step, so it stays at the base size and needs no clamp at all.

Live preview
32011201920
h157.5 px

Type that fits the window

h246.4 px

One declaration per level

h337.5 px

Slope and intercept, not a guess

h430.3 px

From the narrowest phone outwards

h524.5 px

Then it holds steady

h619.8 px

A quiet sub-heading

body16 px

Body copy stays at the base size from the narrowest phone to the widest desktop, because 16 px is already the smallest comfortable reading size and a wider window is no reason on its own to make it larger.

small12.9 px

Caption, footnote, image credit.

The preview uses the rounded numbers from the table, so it shows what the browser would render from the CSS you copy, not from a second, more precise calculation. The panel is also held to the chosen width where there is room for it, so the line lengths are honest too.

WunderPaint
The Dynamic Design and Automation Studio
WunderPaint is a layered image editor for your WordPress media library. WunderPaint Studio is the same thing in any browser, free and without an account.

The middle term is a line, not a percentage

1vw is one hundredth of the viewport width, so font-size: 4vw is a straight line through the origin: 12.8px at a 320 pixel viewport, 51.2px at 1280. You choose the slope and nothing else. Ask for 47.78px at 320 and 61.04px at 1280 and no vw value can do it, because a line through the origin cannot pass through both points.

Add a constant and the line lifts off the origin. Two unknowns now, a slope and an intercept, against two known points, which is exactly enough. The slope is the size difference divided by the viewport difference, times 100 to express it in vw rather than whole viewports. The intercept is what is left of the line at zero width, divided by the root font size to put it in rem.

Target: h1, base 16px, ratio 1.2 at the small end, 1.25 at the large end

  S1 = 16 x 1.2^6  = 47.776 px   at a viewport of  320 px
  S2 = 16 x 1.25^6 = 61.035 px   at a viewport of 1280 px

  slope     = (61.035 - 47.776) / (1280 - 320)
            = 0.0138119 px per px   ->   x100   ->   1.381 vw

  intercept = 47.776 - (0.0138119 x 320)
            = 43.356 px             ->   /16    ->   2.710 rem

  font-size: clamp(2.986rem, 2.71rem + 1.381vw, 3.815rem);

Every number there is checkable in ten seconds. At a 320 pixel viewport 1.381vw is 4.42px and 2.71rem is 43.36px, adding to 47.78. At 1280 the vw part is 17.68px and the total is 61.04.

You do not need calc() inside clamp(). clamp() is itself a maths function and its arguments are already treated as calculations, so 2.71rem + 1.381vw is valid exactly as written. Twenty Twenty-Four ships a real one in its theme.json, on h4: clamp(1.1rem, 1.1rem + ((1vw - 0.2rem) * 0.767), 1.5rem).

That is the same line in point-slope form. 0.2rem is 3.2px, which is exactly 1vw at a 320 pixel viewport, so the bracket is zero there and the size is precisely the 1.1rem minimum. Solve it the other way and something shows up: the bracket has to reach 0.4rem for the size to hit its 1.5rem ceiling, which needs 1vw to reach 0.7215rem, at a viewport of about 1154 pixels. The theme’s wide layout width is 1280. That heading finishes growing 126 pixels before the layout does.

Table of a fluid type scale built from a 16 pixel base with ratios of 1.2 and 1.25, showing h1, h2, h3, body and small with the rendered size at a 320 pixel and a 1280 pixel viewport plus the solved vw and rem terms, alongside the arithmetic that derives the h1 slope and intercept from the two end points.

Why vw on its own fails a zoom test

There is a second reason the constant matters, and it is not about taste. Browser zoom does not scale vw. Zoom to 200 percent and the layout viewport halves in CSS pixels, so a 1280 pixel window reports 640, and every CSS pixel is painted at twice the size. A rem length keeps its value and is painted twice as large, which is the doubling the reader asked for. A vw length halves in CSS pixels, is painted at double, and comes out exactly the physical size it started at. Text set purely in vw does not get bigger when you zoom.

The reader’s own default font size does not move it either. rem is relative to the root font size, so somebody who sets their browser default to 20px gets 25 percent larger text everywhere rem is used and nothing at all where vw is. Page zoom and the default size setting are how most people with low vision actually read the web, and WCAG 1.4.4 asks that text reach 200 percent without the content falling apart.

The rem constant is what rescues that, and the arithmetic is tidy: zooming to 200 percent adds the rem part a second time and leaves the vw part where it was. How much zoom helps therefore depends on how much of the size the constant carries. In the example above it carries 43.36 of the 47.78 pixels at the narrow end, about 91 percent.

Push the end sizes far apart over a short viewport range and the intercept shrinks to zero and then goes negative, which is worse than none: zoom adds that negative constant a second time, so the text gets smaller as the reader tries to enlarge it. A rem term at or below zero means the two sizes are too far apart for the viewport range you gave them.

Spacing is a different question. Twenty Twenty-Five sets several spacing sizes with a bare vw middle term, clamp(30px, 5vw, 50px) among them, which is defensible: nobody needs to enlarge a gap to read it. Type is not a gap.

One ratio, applied eight times

A modular scale is one multiplication repeated. Pick a base size and a ratio, and every level is the base times the ratio raised to its step number. Body is step zero, the small level step minus one, h6 up to h1 steps one to six. With a base of 16 and a ratio of 1.2, h3 is 33.18px and h1 is 47.78px, and neither number was chosen by anybody.

This looks better than eight sizes picked by eye because the eye reads the relationship between consecutive sizes rather than the absolute values, and a constant multiplier keeps that relationship identical at every step. Sizes chosen one at a time drift: two levels land close enough to look like a mistake, or one jumps and the page reads as two designs stitched together. It is the same class of problem as the typography mistakes that give an amateur design away, nothing individually wrong and everything slightly out of relation.

The ratio is where taste enters, and it is the one number the calculator expects you to have an opinion about. It also does not want to be the same at both ends. A ratio compounds, so its effect at step six is dramatic and at step one barely visible. From a 16px base the golden ratio puts h1 at 287px, which on a 360 pixel phone is roughly two characters a line. That is not a headline, it is a logo.

Hence two ratio fields. A smaller ratio at the narrow end keeps the phone readable, a larger one at the wide end lets headings pull away from the body copy once there is room. The defaults, 1.2 and 1.25, are deliberately quiet. A perfect fourth at 1.333 is where most editorial-looking sites sit, and it puts h1 at 89.76px, which needs a wide container to survive.

Two tables: four modular scale ratios from 1.2 to the golden ratio compared at h3, h2 and h1 from a 16 pixel base with the approximate characters per line each h1 allows on a 360 pixel phone, and the contentSize and wideSize layout widths declared by Twenty Twenty-Three, Twenty Twenty-Four and Twenty Twenty-Five plus the 1600 pixel fallback WordPress uses when no wide width is set.

The three numbers a WordPress theme gives you

Three inputs decide the whole scale: the base size, the viewport range, the ratio. The base is usually 16px, the browser default and the size a reader implicitly agreed to by not changing it. The ratio is the taste decision. The viewport range is the one people get wrong, reaching for the screen width when they want the container width.

Type stops needing to grow when the measure stops growing. In a block theme the measure lives in theme.json under settings.layout: contentSize for ordinary blocks, wideSize for wide ones. Twenty Twenty-Four uses 620px and 1280px, Twenty Twenty-Five 645px and 1340px, Twenty Twenty-Three 650px and 1200px. Set your maximum viewport to the theme’s own wideSize and type finishes growing where the layout does. A heading that keeps swelling out to 1920 looks wrong for a reason: the text under it stopped getting wider hundreds of pixels ago.

Core takes exactly that position. Switch on settings.typography.fluid and WordPress builds the clamp() itself in wp_get_typography_font_size_value(), with a minimum viewport width defaulting to 320px and a maximum read from layout.wideSize when the theme sets one, falling back to 1600px when it does not.

{
  "settings": {
    "layout": { "contentSize": "620px", "wideSize": "1280px" },
    "typography": {
      "fluid": true,
      "fontSizes": [
        { "slug": "small",   "size": "0.9rem",  "fluid": false },
        { "slug": "medium",  "size": "1.05rem", "fluid": false },
        { "slug": "x-large", "size": "2.5rem",
          "fluid": { "min": "1.85rem", "max": "2.5rem" } }
      ]
    }
  }
}

Those are Twenty Twenty-Four’s real values, and the pattern is worth copying: fixed sizes at the small end, an explicit min and max for levels that should move, "fluid": false on anything that should not. Give core only a size and it derives the minimum itself with a logarithmic factor, min(max(1 - 0.075 * log2(px), 0.25), 0.75), refusing to go below a floor of 14px. Fourteen is under the 16 this calculator flags, which is a real difference of opinion.

A classic theme has no theme.json, so there the useful button is the one that emits the whole scale as custom properties. Declare them once, reference them everywhere, and put the CSS in a child theme stylesheet so the next parent update does not take it with it.

:root {
  /* base 16px, 320 to 1280, ratios 1.2 and 1.25 */
  --fs-h1:    clamp(2.986rem, 2.71rem + 1.381vw, 3.815rem);
  --fs-h2:    clamp(2.488rem, 2.301rem + 0.939vw, 3.052rem);
  --fs-h3:    clamp(2.074rem, 1.951rem + 0.613vw, 2.441rem);
  --fs-body:  1rem;
  --fs-small: clamp(0.8rem, 0.844rem - 0.056vw, 0.833rem);
}

h1 { font-size: var(--fs-h1); }
h2 { font-size: var(--fs-h2); }

The floor, and the direction of travel

Mobile Safari zooms the whole page when a form control whose computed font-size is below 16px takes focus. It is not styleable and not defeatable from CSS: the reader has to pinch back out. Setting inputs to 16px is the fix, and since inputs commonly inherit the surrounding size, a scale whose small level lands at 14px will trip it wherever that class ends up near a form.

The calculator marks any level whose smallest rendered size falls under 16px in red and names it beneath the table. The word smallest is doing real work there. With 1.2 at the narrow end and 1.25 at the wide end, the small level is 13.33px at 320 and 12.8px at 1280: it shrinks as the screen grows, because a larger ratio at the wide end pushes negative steps down as surely as positive steps up. Its smallest size is at the maximum viewport, so that is the cell that turns red.

The same effect on a heading is a bug, not a quirk. If h1 comes out smaller at your maximum viewport than at your minimum, either the ends went in the wrong way round or the wide-end ratio is below the narrow-end one. The declaration stays valid CSS, because the bounds are always written low first, but the design is inverted: the heading is largest where there is least room for it. Read the two size columns down. Above body, the first number should always be the smaller one.

Type size belongs to the same family as contrast, which is a number rather than an opinion. Both have a threshold that is met or not met, and both get argued about far more than they get measured.

Where the numbers stop

Every rem printed is measured against a 16px root, the browser default, and the note under the table says so. If your theme sets html { font-size: 62.5% } to make mental arithmetic easier, a printed 2.986rem renders at 29.86px instead of the 47.78px it was solved for, and the whole scale comes out five eighths of the size you designed.

The base is the size body gets at every viewport, so body never scales. The ratios only move the levels above and below it. That is deliberate: 16px body copy is 16px on a phone and on a monitor, and readers who need it larger have already told their browser so. If you want the body itself to grow on a wide screen, this is not the tool for it.

Ratios are restricted to 1 through 4. A ratio of 1 at both ends is allowed and gives a flat scale: every level a plain rem, no clamp() anywhere. Body already behaves that way: an identical minimum and maximum is written as a plain rem, not a clamp() holding three copies of one number. Invalid input, a base outside 8 to 200, a ratio outside 1 to 4, a viewport outside 120 to 3840, or a minimum viewport not below the maximum, turns the field red and explains the rule, leaving the last good scale standing rather than blanking the table mid-edit.

Output is rounded to three decimals for rem and vw, two for px and pt, and the preview evaluates those rounded declarations rather than a second, more precise calculation. That is on purpose: the preview shows what the CSS you copied will really do, which occasionally means a level reaches its ceiling a few hundredths of a pixel past the maximum viewport. The slider runs from 320 to 1920 whatever the viewport fields say, so you can see past your own ceiling. Each row is a font-size and nothing more: no line height, no measure, no letter spacing. And the tool cannot appear twice on one page, because its element ids are fixed rather than prefixed per instance.

The value of a scale like this is not the typing it saves, though twenty four declarations collapsing into eight is not nothing. It is that the decisions move to the front. You choose three numbers once, with reasons: 16px because that is what the browser and the reader agreed on, the container width because that is where the text stops getting wider, a ratio because you compared a few and preferred one. Everything else is derived, and derived numbers do not drift.

It is the same shape of decision as building a colour palette from one choice. Pick the seed carefully, let arithmetic produce the rest, and spend your attention on the seed rather than on forty downstream values that will never quite agree. Type and colour are the two systems where that pays immediately, and the two where hand-picked values decay fastest.

Media queries are not the enemy and they do not go away. A sidebar that becomes a stack, a grid that drops to one column, a navigation that turns into a button: those are real changes of state, and a breakpoint is the right tool. Font size is not a change of state. It is a continuous quantity that was implemented with a step function for twenty years because nothing else existed. Something else exists now, and it fits on one line. The exception is text set over a photograph, where the background differs across the frame and readability stops being a size problem and becomes a contrast one.

Type That Fits Every Screen Without a Single Media Query

Table of Contents

Learn it by building something

Every week one thing you can make the same afternoon, from dynamic templates to 3D type. Written down step by step.

One mail a week, and then it ends.
Unsubscribe in one click.

Developer Tools

Encoding Forensics: How to Fix Mojibake Like ü

ü is not corruption. It is one umlaut, two UTF-8 bytes, read by a program that believed they were Windows-1252, then saved again so the wrong reading became the content. Here is how to tell which wrong turn your text took, why copied replacement lists only half work, and what to check in the database before you clean anything.

CSS & Front-End

CSS Gradient Generator: Gradients That Do Not Band

A gradient that looked smooth in the design tool grows stripes on the page. The cause is arithmetic: 8 bit colour, two similar colours and 1600 pixels to cover. How to count the steps before you ship, the three real fixes, and a builder that measures the banding for you.

SEO & Structured Data

WordPress OG Image: The Shop Window You Never See

WordPress core never writes an og:image tag, so a wrong or missing link preview is always somebody else's output. Here is the fallback chain a plugin walks, the registered size trap that makes a valid tag point at a missing file, and two curl commands that settle it.

Troubleshooting

WordPress Missed Schedule: Why WP-Cron Is Not a Cron Job

WP-Cron has no timer and no background process. It is a list of due tasks that gets checked only when somebody loads a page, which explains missed post schedules, stalled backups, and every other scheduled task that silently stops.

Design Fundamentals

White Space Is Not Empty: The Composition Rules That Make Anything Look Designed

Change the logo, the photo and the button color and a bad layout usually stays bad, because the fault was in the gaps rather than the parts.

Developer Tools

Text Cleaner: Getting the Junk Out of Pasted Text

Pasted text carries more than words: Word markup, PDF line breaks, tracking parameters, and a set of characters that render as nothing at all. What each one actually is, why WordPress makes some of it worse, and what to strip.

Download the free WunderPaint Plugin for WordPress

The WunderPaint workspace with the layers panel, adjustment sliders, text style presets and the asset library along the bottom

The Image Editor & Design Studio

Everything described here can be done in the browser, on your own site. The live demo runs the full editor with nothing to install.

Free

Chaos Art

Autonomous painters make one-of-a-kind abstract art in 3D space - gestures, art movements, painterly media, and embeds that paint a new original for every visitor.

Pro

Particle Strokes

Paint with swarms of light: twenty-two movements, a stamp you draw yourself, and curves that give a stroke a shape - the swarm keeps painting for a few seconds after you let go.

Pro

City Diorama

Any place on earth as a miniature you could hold: real streets, water and building footprints raised into a 3D diorama - or wrapped around a sphere as your own tiny planet.