Fluid typography with CSS clamp(): a practical guide
Originally published at maniruzzamanjubayer.com.
Fluid typography with CSS clamp() takes two font sizes and two screen widths and turns them into one line: clamp(smallest, preferred, largest), where the preferred value mixes rem and vw so the size grows in a straight line between the two screens. For a heading of 32px on a 360px phone and 56px on a 1440px laptop, the answer is clamp(2rem, 1.5rem + 2.2222vw, 3.5rem). The part most clamp generators skip is zoom: if too much of that value is vw, the text can't be enlarged to 200%, and the scale fails WCAG 1.4.4.
Below is the maths, so you can check any clamp calculator's output by hand, then the zoom problem and how to avoid it, a fluid type scale, Tailwind v4, and my free CSS clamp calculator, which builds a whole scale and checks every step for zoom.
Key takeaways
- clamp() resolves as max(MIN, min(VAL, MAX)), and it has worked in every major browser since July 2020.
- The preferred value is an intercept in rem plus a slope in vw, both worked out from your two sizes and two screens.
- Browser zoom grows rem but not vw, so a size made mostly of vw can't reach twice its size.
- Keep each step's largest size at or below 2.5 times its smallest, and it passes WCAG 1.4.4 in modern browsers.
- In Tailwind v4, put the clamp() values in @theme as --text-* variables and use them as normal classes.
How clamp() works
MDN's clamp() reference says clamp(MIN, VAL, MAX) is resolved as max(MIN, min(VAL, MAX)). The middle value is used while it sits between the two bounds; below the smallest it holds at MIN, above the largest it holds at MAX. MDN marks it Baseline, widely available, across browsers since July 2020, so you don't need a fallback for current browsers.
For type, that gives you three parts:
- MIN: the size on your smallest screen, in rem.
- VAL: a size that changes with the viewport, written as
intercept rem + slope vw. - MAX: the size on your largest screen, in rem.
Fluid type replaces the jumps you get from media queries. Every width between your two screens gets a size on the straight line between your two sizes.
The clamp calculator maths, step by step
Take the heading above: 32px at 360px wide, 56px at 1440px wide, with a 16px root.
- Slope = (largest size minus smallest size) divided by (largest screen minus smallest screen) = (56 minus 32) / (1440 minus 360) = 24 / 1080 = 0.02222.
- vw part = slope times 100 = 2.2222vw. One vw is 1% of the viewport width, which is why the slope is multiplied by 100.
- Intercept = smallest size minus slope times smallest screen = 32 minus 0.02222 times 360 = 24px.
- rem parts = px divided by the root size: 24px is 1.5rem, 32px is 2rem, 56px is 3.5rem.
Result: font-size: clamp(2rem, 1.5rem + 2.2222vw, 3.5rem);
Check it at a 900px screen: 24 + 0.02222 times 900 = 44px. That is what the browser shows, and it lands on the straight line between 32 and 56.
If you want a step that gets smaller on large screens, the same maths works: the slope turns negative and the two bounds swap places, because clamp() needs the smaller value first.
Want it in code? This is the whole calculation in JavaScript:
function fluid(minPx, maxPx, minVw, maxVw, root = 16) {
const slope = (maxPx - minPx) / (maxVw - minVw);
const intercept = minPx - slope * minVw;
const r = (n) => +(n / root).toFixed(4);
const [lo, hi] = [Math.min(minPx, maxPx), Math.max(minPx, maxPx)];
return `clamp(${r(lo)}rem, ${r(intercept)}rem + ${+(slope * 100).toFixed(4)}vw, ${r(hi)}rem)`;
}
(It prints a + even when the slope is negative; tidy that if you need shrinking steps.)
Why rem matters for zoom
There are two ways people make text bigger: page zoom, and the default font size setting in the browser. Page zoom grows px and rem alike. The font size setting only moves rem, because rem is relative to the root font size, which that setting changes. px ignores it. vw ignores both.
So write MIN, MAX and the intercept in rem. That keeps the scale tied to what the visitor asked for. The vw part can't be tied to anything; it follows the window. W3C lists this as a known failure: F94, incorrect use of viewport units to resize text, because viewport units "cannot be resized by zooming or adjusting text-size".
One trap here: if your CSS sets html { font-size: 62.5% }, a rem is 10px, not 16px. Tell the generator your real root size or every value will be off.
WCAG 1.4.4 and the zoom problem in fluid type
WCAG 2.2 Success Criterion 1.4.4, Resize Text, is Level AA: text, except captions and images of text, can be resized without assistive technology up to 200 percent without loss of content or functionality.
Fluid type makes that harder than it looks. Maxwell Barvian set out the problem in Addressing Accessibility Concerns With Using Fluid Type in Smashing Magazine (7 November 2023). When you zoom, the browser scales rem and px but not viewport units. So at 500% zoom, the common browser maximum, the rem part of the size is five times bigger while the vw part stays roughly where it was. If the vw part is most of the size, the text can never double.
His worked example, a size from 16px to 48px, reached only about 1.67 times its size at 500% zoom over part of the range, failing roughly between 1050px and 2100px wide screens. I ran the same sizes through my tool's zoom check (16px at 320px to 48px at 1280px) and it flags 1050 to 2070px, which matches.
His rule of thumb is the useful part: if the largest size is no more than 2.5 times the smallest, the text always passes 1.4.4 in modern browsers. Miriam Suzanne at OddBird repeats it in Visualizing Responsive Typography (26 August 2025). The heading above, 32px to 56px, is 1.75 times, so it passes. A display size from 24px to 72px is 3 times; my tool flags it on screens from 1180px to 2330px.
When a step fails:
- Raise the small screen size, so the ratio drops under 2.5.
- Lower the large screen size or the large screen scale ratio.
- Test with real browser zoom. Barvian notes that DevTools doesn't show zoomed values, so screenshots are the honest check.
The same limit applies to type that jumps at media queries: a jump of more than 2.5 times can fail in the same way. I cover zoom with the rest of my checks in WCAG 2.2 in React.
From one value to a fluid type scale
One clamp() fixes one heading. A fluid type scale fixes them all. Pick a base size and a ratio for the small screen, and another pair for the large screen, then multiply up and down. Each step gets its own clamp().
A smaller ratio on phones keeps headings from swamping a narrow screen; a larger one on laptops gives clearer hierarchy. My tool's default is a 16px base with a 1.2 ratio (minor third) at 360px, and an 18px base with a 1.25 ratio (major third) at 1440px. That gives, for example:
| Step | 360px | 1440px | clamp() |
|---|---|---|---|
| step-0 (body) | 16px | 18px | clamp(1rem, 0.9583rem + 0.1852vw, 1.125rem) |
| step-2 | 23.04px | 28.13px | clamp(1.44rem, 1.3341rem + 0.4708vw, 1.7578rem) |
| step-5 | 39.81px | 54.93px | clamp(2.4883rem, 2.1734rem + 1.3999vw, 3.4332rem) |
Every step of that default passes the zoom check, because no step grows by more than about 1.4 times.
Utopia, by James Gilyead and Trys Mudford and supported by Clearleft, popularised this approach; its type calculator is worth reading even if you never use it. Type is one family of design tokens; my guide to design tokens covers how they move from Figma variables to code, and the type section of UI design trends 2027 covers variable fonts.
Fluid type in Tailwind CSS v4
Tailwind v4 reads font sizes from theme variables. The Tailwind font size docs show --text-* variables inside @theme, with optional --text-*--line-height, --letter-spacing and --font-weight companions. A fluid scale looks like this:
@import "tailwindcss";
@theme {\ --text-step-0: clamp(1rem, 0.9583rem + 0.1852vw, 1.125rem);\ --text-step-0--line-height: 1.5;\ --text-step-2: clamp(1.44rem, 1.3341rem + 0.4708vw, 1.7578rem);\ --text-step-5: clamp(2.4883rem, 2.1734rem + 1.3999vw, 3.4332rem);\ --text-step-5--line-height: 1.1;\ }\
Then class="text-step-5" works like any built in size. For a one off, Tailwind documents arbitrary values as text-[<value>]. Class names can't hold spaces, and the + inside clamp() needs them, so write them as underscores: text-[clamp(2rem,1.5rem_+_2.2222vw,3.5rem)]. I'd keep one offs rare; a named step is easier to review.
Use a unitless line height with fluid sizes, so the leading scales with the text.
Alternatives, honestly
- Write it by hand with the four steps above. Fine for one or two sizes.
- Media queries or Tailwind's
md:text-xlstyle breakpoints. Simple and predictable, but sizes jump, and you still need to watch the 2.5 times limit. - Utopia's calculators for type and space, if you want fluid spacing to match.
- Other online clamp generators. Most turn one pair of sizes into one clamp(); check whether they use rem and whether they say anything about zoom.
How my free clamp calculator works
The fluid typography calculator does the maths above for a whole scale or a single value. You set the smallest and largest screen, the base size and scale ratio for each, how many steps up and down, and your root size. It shows:
- a clamp() for every step, using the slope and intercept formulas above;
- a live preview of the scale at any screen width;
- a zoom column that checks each step on screens from 320 to 3840px, passing where some zoom up to 500% reaches twice the size at 100%, and naming the widths where it fails;
- output as CSS variables, Tailwind v4 @theme, Tailwind v3 config, SCSS or JSON.
Named ratios run from minor second to the golden ratio, or you enter your own. Step names can be step-0, t-shirt sizes or heading names. It runs entirely in your browser. If you already keep tokens in a file, the design tokens to Tailwind converter takes the whole file.
From my work
I set the type scale before the first screen, as tokens named the same in Figma and in code, and I check the largest steps at 200% zoom on a real browser before handoff. Display headings are where the 2.5 times limit bites first.
Frequently asked questions
How do I calculate clamp() for font size?
Slope = (max size minus min size) / (max screen minus min screen). Intercept = min size minus slope times min screen. The value is clamp(min, intercept rem + slope times 100 vw, max), with sizes in rem.
Is fluid typography accessible?
It can be. The risk is zoom, because vw doesn't grow when people zoom. Keep each step's largest size at or below 2.5 times its smallest and check the result with browser zoom.
Why use rem instead of px in clamp()?
rem follows the visitor's default font size setting and page zoom. px follows zoom but ignores the setting, and vw follows neither.
What screen widths should I use?
A common pair is 360px for the smallest phone and 1440px for a typical laptop. Outside that range the sizes hold at MIN and MAX.
Does clamp() work in Tailwind v4?
Yes. Put the values in @theme as --text-* variables and use the classes, or use an arbitrary value such as text-[clamp(...)] for a one off.
Does fluid type replace media queries?
For font sizes, mostly. You may still change layout and line length at breakpoints.
Sources and method
I read every source on 1 November 2026 and ran the worked examples through my tool's own calculation code on the same day.
- MDN, clamp(), date read 1 November 2026: developer.mozilla.org
- W3C, Understanding SC 1.4.4 Resize Text (WCAG 2.2), date read 1 November 2026: w3.org
- W3C, Failure F94, incorrect use of viewport units to resize text, date read 1 November 2026: w3.org
- Maxwell Barvian, Addressing Accessibility Concerns With Using Fluid Type, Smashing Magazine, 7 November 2023, date read 1 November 2026: smashingmagazine.com
- Miriam Suzanne, Visualizing Responsive Typography, OddBird, 26 August 2025, date read 1 November 2026: oddbird.net
- Tailwind CSS, font-size documentation, date read 1 November 2026: tailwindcss.com
- Utopia, date read 1 November 2026: utopia.fyi
If your type scale is one part of a bigger design system that needs designing and building, my design engineering page shows how I work, and every project starts with a free 30 minute intro call.
About the author
- I'm Maniruzzaman Jubayer, a design engineer in Dhaka, Bangladesh. I design web and mobile products and build them in production code, from the first flow to launch, for founders in the US, the UK, Europe and the Middle East. I write here about product design, design systems, RTL and Arabic interfaces, accessibility and Next.js.
Comments
Join the discussionNo comments yet. Be the first to comment.