Better fittext

How could you say that? You think you can do better?

Posted July 4 2011
JavaScript, Typography, Web
Tweet this article

A few weeks ago I heard about the fittext.js script cre­ated by Par­avel. I’ve had it in the back of my mind as some­thing I wanted to try. In redesigning the theme for this blog, I found a small way in which to incor­po­rate it: the ‘not found’ page.

I always appre­ciate it when a web­site does some­thing inter­esting with the not found page. They can be humorous, sar­castic or cute. I wish I remem­bered a few of the good exam­ples. Often not found pages are very simple (though well done; see Owl­tastic). Some­times they’re just for­gotten (like on Paravel’s web­site; sorry to pick on them). I wanted a simple but bold page. How about a sign that just says “wrong way”? Inci­den­tally, Hypatia Sans Pro, the font I’m now using on my web­sites thanks to the folks at Extensis, has a really won­derful cap W.

Here’s the end result. Unfor­tu­nately, it took me a long time to get this. Fittext.js, as it turns out, is pretty weak.

How could you say that? You think you can do better? Well, I did, and here’s an exam­i­na­tion of how. First, a little bit of code from fittext.js:

var resizer = function () {
$this.css('font-size', Math.min($this.width() / (compressor*10), origFontSize));
};

This is really all there is to fit­text. At first I was sur­prised. How could this create text that fills a line? Well, the answer is that it can’t. Not really. All this func­tion does is divide the pixel width of the block ele­ment in which the text appears by 10, and then com­pares that value to the existing CSS font size set­ting. Whichever one is smaller is the size that the font is set to. In order to give the user some con­trol over the math, they added the com­pressor vari­able. This is a value the user can supply as a func­tion argu­ment which will adjust how the new font size is calculated.

There are a number of prob­lems here. First, the default font size must be large. The func­tion uses the smaller of the two values as the font size. If the orig­inal size is very small, fittext.js won’t do any­thing. Second, the idea of dividing the block width by 10 is arbi­trary. This is why the com­pressor vari­able is avail­able. In order for text to actu­ally fit the user must play with the com­pressor vari­able to find the right value. Finally, this is font spe­cific. Because each font has a dif­ferent em width, you will need dif­ferent com­pressor values for dif­ferent fonts. You better be sure the selected font is avail­able, oth­er­wise your text will be too large or too small.

While I appre­ciate the sim­plicity of this low band­width approach, I wanted some­thing more robust, more exact, and more bul­let­proof. My solu­tion is there­fore more com­pli­cated in it’s exe­cu­tion. How­ever, the math is really very simple. It’s about proportions:

a:b = c:d or a/b = c/d

You remember pro­por­tions from high school, yes? The rela­tion­ship of a to b is equal to the rela­tion­ship of c to d. We can use this expres­sion to fit text using the assump­tion that the width of a line of text is lin­early pro­por­tional to the font size. As it turns out, this assump­tion works well. Given a block ele­ment of a par­tic­ular width, I can find the point size that will make a line of text equal to the width of the block like this:

origFontSize / newFontSize = currentLineLength / blockElementWidth

The orig­inal font size, cur­rent line length, and block ele­ment width can all be deter­mined with jQuery. Rear­ranging the equa­tion above to iso­late the unknown vari­able I get:

newFontSize = (blockElementWidth / currentLineLength) * origFontSize

Finding the cur­rent line length is a little tricky. The eas­iest way I found is to tem­porarily set the block ele­ment to float (float:left). Because floats are required to have a width, the browser auto­mat­i­cally sets the width of a floating ele­ment to the width of its con­tents (assuming no width has been spec­i­fied. If you have a line of text inside a p tag and want to know how many pixels long that line is, float the p ele­ment and then check the width (it helps if your text is styled white-space:no-wrap) Here’s the basic jQuery code:

1 var limit_w = $(textfits[x]).width();
2 var current_w = $(textfits[x]).css('float','left').width();
3 var new_emsize = parseInt($(textfits[x]).css('fontSize')) * (limit_w / current_w) / 10
4 $(textfits[x]).css({'fontSize':new_emsize+'em','float':'none'});

In this code, “textfits[x]” is a ref­er­ence to the block ele­ment in which I want text to fit. The ‘10’ at the end of the third line is used to con­vert the font pixel size to an em size (my doc­u­ment has 10px to 1em).

Oh, but there are caveats

Despite the fact that my math is sound, this approach did not work at first. The reason? I am using web­fonts from Extensis. Web­fonts are not low band­width; they take time to load. Even though I had placed this script at the bottom of my page, the script ran before Hypatia Sans Pro has a chance to load. The script was using the fall­back font, Georgia, in the cal­cu­la­tion of the text size. Because Georgia is much wider than Hypatia Sans, my text was not a per­fect fit. In order to make the script work per­fectly, I had to attach it to the window.onload event han­dler. (People using web­fonts from TypeKit, Ascender, Google, or other ser­vices that deliver CSS files might be able to use Google’s Web­Font loader to check for loaded fonts. Extensis pro­vides links to the actual font resources.)

Of course run­ning the script onload caused a “jump” which is prob­ably familiar to many devel­opers. The page, with the unfit text, is fully loaded before my fit­text script runs, meaning it is vis­ible in its unfitted state for a moment before the font size is cor­rected. Some people might not think that’s a big deal, but it looks unpro­fes­sional. The quick solu­tion? Hide the unfitted text using CSS (visibility:hidden) and then show it after the script has run.

Whew. Much effort for a simple not found page. My wife thinks I’m crazy, but I learned some new things along the way. They say it’s the journey that counts, right?