Vladimir Prus


vladimirprus.com

Thursday, July 31, 2014

Text Baselines in HTML

I usually write HTML as part of a some quick hack, often personal, so I take every shortcut I can. But one thing I cannot stand is misaligned text - where two text elements appear next to each other horizontally, but their text baselines are not perfectly aligned. This is a much-discussed topic, but somehow most of the solutions that professional designers discuss don't work for me.

For example, take a blog post called Setting Type on the Web to a Baseline Grid. It's fairly detailed about global structure of the page, which is indeed nice, and it comes with an example, and when you look closely, you see this clear misalignment:


Incidentally, aligning baselines of two text elements with different font size is what I wanted to do yesterday, so let's see how to do it, starting with a case that works just fine. Here are the essential parts of HTML:
<body>
    <div>
        <span class="big">Hello</span>
        <span class="small">World</span>
    </div>
</body>

The way it looks, with boxes added for exposition, is this:


Each of the span elements creates a box with text. The height of each box is equal to CSS font-size property - which is 36px for the first box and 24px for the second. They are then laid out using inline formatting context that by default aligns baselines of each box - so the smaller box is moved down. If you examine the example 1 you'll see that second span element as offsetTop property of 11px - which is exactly what is needed to align baselines, helpfully computed by the browser.

If we were to compute that magic value of 11 ourselves, we'd be in trouble. The CSS font-size property effectively only tells the height of the text box. Although the size of actual letters (x-height and cap height) is proportional to box height, the proportion depends on the font. The position of the text baseline in the text box also depends on the font, so we have nothing to work from. For a very detailed discussion, see Point Size and the Em Square: Not What People Think. There are libraries that try to compute font metrics, such as Font.js, but they do it by drawing font on the canvas and then looking at pixels - rather roundabout way.

If it's hard to compute the right offset for the smaller box, maybe we can make the smaller box have the same height? There is the line-height CSS property indeed, which can be larger than font-size, but it adds the extra height equally on the top of the bottom. In our case, it will add (36-24)/2=6 pixels at the top, very different from the desired 11, as shown by example 2 and the below screenshot.


The bottom line is that when text elements with different font size are inside single inline formatting context, they are perfectly aligned by default, and replicating such alignment manually is hard. But what if want alignment outside inline context - for example if I want to send the second text element to the right?

Floats

One way to send an element to the right is "float: right" property, but example 3 looks rather bad.


This is not a browser bug - CSS spec is clear that when element is floated to the right in inline context, it will be aligned to the top, and this behavior cannot be changed. As explained above, shifting the element manually is rather unpractical. What we can do though, is to add a strut - text element with the large font size, serving only to force the alignment. Here's example 4 and a screenshot:


The final observation is that if inline box has no content, the browser adds true strut - of zero width. So, here's example 5 - with all borders removed, just perfectly aligned text


Using positioning

Instead of floats, one can use either "position: relative" or "position: absolute" on the second span. Using absolute position is fairly easy - with "right: 0" it will be moved to the right border of the container (example 6) but also move to the top - just like happened with float. Strut can be used to fix this, likewise. When using relative positioning, we don't need to do anything about baseline - everything remains aligned by default. However, we need to figure out relative horizontal shift - not too hard, but requires JavaScript (example 7).

Using text alignment

I have intentionally used a simple example here, but if displaying two words on different sides of a page is exactly what you need, you can just give width to both span elements, so that they cover entire width, and then make the second element right-aligned.

Conclusions

It is unfortunate that CSS neither supports baseline alignment across different blocks, nor exposes font metrics is a usable way. If you know exactly what fonts are used, you can compute offsets by hand. In other cases, use struts or relative positioning. 

No comments: