Text reflow on the web has an interesting relationship with Responsive Web Design. As a column gets smaller text wraps and becomes taller1. But for large format display text, that’s not always what you want2. What I’ve wanted for awhile now is a way to inversely size text based on the text length (where the font-size gets smaller as the heading gets longer).

I’ve been chasing the white whale of responsive text-sizing for over a decade and I think I’ve got my best attempt to-date with the new attr() upgrades that landed in CSS (Chromium-only at time of writing).

See the Pen inverse text-sizing with attr by Dave Rupert (@davatron5000) on CodePen.

CSS’s attr() function got an upgrade this year where you can use it independent of content in pseudo-elements. They also added the ability to cast types to the values. Before that change all attributes were text strings and if you tried to use it in a calc() function, CSS would bail because you can’t multiply a pixel by a text string. With the new upgraded attr() function you can specify a type in the new type() function as the second parameter of attr(): attr(foo type(<number>)) .

Having these pieces of technology in place lets us mix in HTML attributes to our CSS math.

h1 {
  --ideal-cpl: 45;  
  --numchars: attr(data-numchars type(<number>));
  --ratio: calc(var(--ideal-cpl) / var(--numchars));
  --min-font-size: 1.2rem;
  --max-font-size: 4rem;
  
  font-size: clamp(
	  var(--min-font-size), 
	  var(--max-font-size) * var(--ratio), 
	  var(--max-font-size)
  );
}

We start with an --ideal-cpl, which is a typographic notion that there’s an ideal line-length at or near 66 characters per line and perhaps smaller on mobile (source: UXPin). Headings are probably less. We take that number and divide it by --numchars from our data-numchars attribute to get a ratio to multiply our text by (e.g. 45cpl / 60numchars = 0.75 or 45cpl / 30numchars = 1.5).

Then set some min/max variables and chuck it all into a clamp() function to enforce some limits. Anything under the --ideal-cpl will clamped to the --max-font-size and titles over the --ideal-cpl will shrink down until they hit the --min-font-size.

To help with graceful degradation in unsupported browsers (Safari, Firefox), we need to use the CSSOM @property syntax to typecast our --numchars custom property. This is a hint to browsers like Mobile Safari that --numchars is going to be a <number> and we can set a fallback value as well.

/* Property used to provide fallback to other browsers */
@property --numchars {
  syntax: "<number>";
  inherits: false;
  initial-value: 45; /* set to same value as --ideal-cpl */
}

Hopefully the “magic” here is pretty easy to understand but for completion’s sake, you’ll need to add data-numchars to your HTML to make this work.

<h1 data-numchars="71">
  Lorem ipsum dolor sit amet consectetur
  adipisicing elit. Vitae, maxime.
</h1>

Templating languages can help. Here’s what I do in my Liquid templates for Jekyll:


<h1 data-numchars="{{ title.size }}">{{ title }}</h1>

If you don’t have access to your template but somehow have access to JavaScript, you can do something this:

document.querySelectorAll('h1').forEach(el => 
	el.dataset.numchars = el.textContent.length
);

I’ve went ahead and rolled this technique out on my entire site and made a All Titles page where I can tweak variables to my liking. The result is I get to keep my chonky headlines on mobile, but almost no heading wrap more than three lines. One improvement might be to use more CSS math to snap to different short/medium/long sizes, so its less cacophonous… but on my site you only see one display title at a time, so it’s less of a big deal.

Scaling text responsivebly

With all that in place, we have a HTML/CSS-based solution for responsive text-sizing with minimal overhead, no JS required, no extra DOM, and a graceful degradation to older browsers. I intentionally kept this dumb to allow for text wrapping, but Paul Irish forked me to make a contenteditable demo work edge-to-edge like Kizu’s Fit-to-Width or Zach Leatherman’s old BigText jQuery plugin.

See the Pen inverse text-sizing with attr - bigtext.js style - contenteditable dealio by Paul Irish (@paulirish) on CodePen.

Impressive. There’s lots of ways to resize text responsively and I’ve added another one to the pile, so here’s a little guide with how I’m thinking about all the options.

  • If you want exact edge-to-edge text-sizing I’d probably recommend Kizu’s clever Fit-to-Width technique.
  • If you (and your designers) are okay with not-so-exact edge-to-edge sizing, Paul’s approach seems like a good candidate.
  • Don’t forget about SVG. Converting display text to outlines in Figma and exporting as SVG is also a nice option for exact fitting text one-offs, but doesn’t localize well.
  • If you want text to scale-inversely to its length but still wrap, use the approach in this post.
  • If you want text to be a bit more fluid between breakpoints, use clamp() + vw units.
  • If you want text to scale based on it’s parent’s width like a vector, then DON’T USE FitText and use clamp(var(--min-font-size), 10cqi, var(--max-font-size)) with a CSS container instead.
  • If you want strict typographic control at every breakpoint, use rem or px.

Lots of options for scalable type out there that give type-setting control freak nerds nightmares, have fun!

  1. Images have the opposite problem, wide desktop images lose hierarchy as the viewport gets smaller. And then portrait images get too much hierarchy when they scale up.

  2. Large format display text wrapping on mobile can even cause accessibility issues, whether it’s cognitive issue with one word per line or a motor issue where long words break the container and cause horizontal scrolling.