On CSS baseline and vertical rythms

A crafty and hacky implementation of CSS baseline, transforming lineheight behavior, and feeling of rythms.

Craft context

I started to go deeper in CSS baseline related crafts while designing myself a personnal planner. The planner was constructed around a 0.5cm grid with bullet-journal like dot patterns, and was entierly made with HTML and CSS. When writting with a pen on a quadrillated paper sheet, the grid becomes a nice invitation to write your text so that its baseline matches the horizontal lines of the grid. Because the planner would contains both printed typography and my own handwriting, I wanted to see how the baseline of HTML text could sit on the vertical grid of the planner. That curiosity became a challenge: can I do that so that it magicaly works for any font-familly and font-size by using CSS. Implementing such a design goes against the native way text flows in a webpage and it requires to unfold some complexities of CSS typography. It is important that the context was printing, doing something this complexe for a website is a fragile path.

All those complexities that we're about to unfold come from the fact that the web revolves around different rules than desktop pubishing softwares when handling baselines. The concept of leading and line-height differs. Both tools left different type of cuts on the materials and have proven to create usable reading environment, so this is no place for typography arguments but rather for crafts.

A comparison of leading from classic DTP softwares and line-height in CSS
A comparison of leading from classic DTP softwares and line-height in CSS

Different rythms

The fluidity of default

Let's take at look at the Default vertical rythm of HTML & CSS. In the absence of any kind of explicitly defined grid the flow is controled by the user-agent stylesheet.

A two-columns example of the Default vertical rythm of HTML & CSS
A two-columns example of the Default vertical rythm of HTML & CSS. It is set in Junicode. The text and image are taken from Wikipedia.

In this case: line-height is unit-less (here it has been adjusted to 1.35 for the paragraph, and 1 for the headers for comparison purposes, while the default is often 1.17) and vertical margins are defined in em (here they have been adjusted to 1em or 2em values, while the default is often 1em). Those defaults doesn't really care about alligning anything on a grid, as we can see by the two highlighted paragraphs.

In Default vertical rythm:

This is easily explained because unitless line-height vary according to the element's font-size (like if it was set in em). So it produces bigger line-height for a h2 than for a p: every element with different font-size having different heights. The spaces in between the boxes are also not regulated and font-size dependant.

Changing the font-size of an element impact the rest of the document, everything stay fluid and a modification in styling induces a modification of flows in a continuous manner.

The default way a webpage react to font-sizes live editing
The default way a webpage react to font-sizes live editing.

The magnetism of restriction

If you've ever done print publishing using web-standard you've probably encountered this. Constant vertical rythm is done by setting certain values to explicit multiples so that a defined rythmes is able to repeat.

A two-columns example of a Constant vertical rythm layout
A two-columns example of a Constant vertical rythm layout.

This approach is often desired when you have text flowing on two columns, because it will make side by side things look aligned. However this rythm is also sometimes used on the web (by more nerdy or intricate designer or typographer) to get a magnetized feeling in the rythm of a page.

In Constant vertical rythm:

Changing the style of the elements is independant of the flow of the rest of the document.

How _Constant vertical rythm_ react to font-sizes live editing
How Constant vertical rythm react to font-sizes live editing.

This make it easy to adjust your font-size live through the inspector, without creating misalignment in the rest of the document, which again can be nice for printing. Note that if you have big font-sizes, it's possible that some texts vertically overflow their HTML element. If such text as to fit in more than one line, it can become handfull to set its line-height to be the double of the other (as explained here: css-baseline-the-good-the-bad-and-the-ugly)

The craft of alignement

But as stated, what we want to achieve is a bit more complex than Constant vertical rythm...

In a Base-aligned rythm:

A two-columns example of a Base-aligned rythm layout
A two-columns example of a Base-aligned rythm layout.

The subtetly is that the part that is aligned with the grid is the top and bottom of the elements's boxes, not their baseline. But we could hope for even more: to set up a base-aligned system that, in term of font-sizes changes, works so that it doesn't un-aligned any element before or after and the element stays base-aligned. At this point you could be asking yourself "But, would it be possible to have an implementation of base-aligned rythm that work in CSS only, without having to manually edit or translate each different styling?".

(the answer is yes)

How a _Base-aligned rythm_ react to font-sizes live editing
How a Base-aligned rythm react to font-sizes live editing.

Crafting a constant vertical rythm

In order to be able to do a Base-aligned rythm, we first need to implement Constant vertical rythm. While there exists different way to do so, the basic mechanism is the following. A basic height unit is choosen, you can think of it has the smallest step of your vertical grid. It is often equal to the line-height of a standard p, or a fraction of it if we want to go more granular.

For every elements that compose the typographic flow:

  1. If their heights are determined by their text content (like p, h2, ul, ...), they must have a line-height which is a multiple of the height unit (often equals).
  2. If their heights are not determined by their text content (like img, aside with fixed height, ...), they must have a fixed height which is a multiple of the height unit.
  3. the sum of their spacing, meaning the margin + border-width + padding that separate each combination of two elements, must also be a multiple of this height unit.

We see that if this is respected, then this implies that the height and spaces between every elements that compose the typographic flow are equal to a multiple of the height unit, and thus matches our vertical grid.

I love to craft this implementation by introducing a global --lh CSS custom properties which define the height unit of our vertical grid. We can then start using this properties as our new unit:

The actual CSS unit that we used for --lh is also an important choice: rem, meaning its going to be relative to the font-size defined in the html tag, unlike em which adapts to the current element font-size. The idea is that by using rem we keep it independant of the elements indivual font-size's, ensuring this value is global over the document, but we stay flexible: by changing the font-size of the html tag, from 1em to 16px to 12pt for example, it'll automatically adapt our --lh to switch unit and size while keeping the same relative ratio.

Another neat craft is to use a class (here .cvr), this is so we can target parts of the page to follow a constant-vertical rythm by putting this class on the container; and things like menus, buttons, aside, or other are not going to be messed up and can still have their independant line-height, like probably of 1 after a reset. This implementation, could be used for both print and web and is relatively easy to implement.

:root{
    --lh: 1.35rem;

    /* font-sizes */
    --fs-big: 2.6rem;
    --fs-plus: 1.4rem;
    --fs: 1rem;
}

html{
    /* main font-size, 
    has to be defined in the html tag 
    so we can use the rem unit for --lh */
    font-size: 1.125em;
    /* this value upscale 16px to 18px */
}

.cvr{
    font-size: var(--fs);
    line-height: var(--lh);
}

/* font-sizes */
.cvr h1{
    font-size: var(--fs-big);
    line-height: calc(var(--lh) * 2);
}
.cvr h2{
    font-size: var(--fs-plus);
}

/* spaces */
.cvr :is(h1,h2){
    margin: calc(var(--lh) * 2) 0 calc(var(--lh) * 1);
}
.cvr p{
    margin: calc(var(--lh) * 1) 0;
}
.cvr img{
    display: block;
    height: calc(var(--lh) * 5);
    margin: 0 auto;
}

Background on a span and dreaming about lh

Looking at the previous implementation we can start to wonder. An interestning way to interpret it, is to see it as the implementation of a custom CSS unit. Like em (typography relative) or px (screen relative), we introduced a new typographic relative unit --lh, and we count this unit by using calc() which of course doesn't look super appealing.

/* writting this */
margin: calc(2 * var(--lh)) 0;

/* is conceptual like writting this (which doesn't work) */
margin: 2lh 0;

As a reminder the em is a conceptual square where its size have the lenght of the font metrics from ascender to descender. So let's say that in the font's metric ascender is at 624 and descender is at -400, then the em square is a virtual square of 1024 font units. When we say fontsize: 16px; in CSS what we say is: "make the em square 16 pixels", meaning from ascender to descender it's going to take 16px, it's like saying "make the conceptual invisible global bounding box of the glyphs in my font take 16px". This shows that em is not really a visual unit, fontsize: 16px; doesn't make anything visual on your screen take 16px;. By declaring "the vertical margins are equal to (or a fraction of) the em-square" we end up with margins equal to an invisible conceptual square induced but the crafting of the font itself. But in fact there is one visible element taking 1em: the selection pseudo-element background that you can see by selecting text, or more generally by putting background on a <span> (or any inline element).

While using --lh as margins, we're declaring "the vertical margins are equal to exactly (or a fraction of) an empty line of text". Our eyes percieved it in a different way, it feels like you would have put a <br/>, like simply pressing enter on a simple text editor. Wether one would like to use an invisible line or an invisible bounding box of a glyphs as a spacing unit is a matter of typographic desired feeling that mixes history of lead characters and digital text editor. Anyways, a cool thing about the previous --lh unit implementation is that it became available a lot of different places, UI elements heights, or even horizontally.

Displaying the grid

:root{
    --lh: 1.35rem;
    --vg-width: 1px;
    --vg-color: cyan;
}
.show-baseline {
    background:
        linear-gradient(white rgba(0,0,0,0),
        rgba(0,0,0,0) calc(var(--lh) - var(--vg-width)),
        var(--vg-color) calc(var(--lh) - var(--vg-width)),
        var(--vg-color) var(--lh));
    background-size: 100% var(--lh);
}

Crafting a Base-aligned rythm

Naïve approaches

Starting from a Constant vertical rythm, a naïve approach would be to vertically translate everything, let's say with either a margin-top or padding-top and hope every baselines match the grid, which fails because every elements with different styling may need to be shifted from a different value.

A two-columns example of Constant vertical rythm where every elements is vertically translated from a same value
A two-columns example of Constant vertical rythm where every elements is vertically translated from a same value.

So we could precise a different vertical translation value for every elements, as explained in this article css baseline the good the bad and the ugly. But this is not the solution we are looking for. Not only because it's long to setup, but also because what we want is get the system right and then think outside of the system we've made. With this solution, everytime we change the font-size of a elements, we would have to adapt this value so it matches with the grid again. Having to adjust the system everytime we move something makes our thought oscillate between what we are doing and how to do it.

Futhermore putting this shift value in em for every element as part of their margin-top doesn't solve it either. As the line-height is fixed - not relative to the font-size of the elements - and some element have different font-size, it can gives us an instinct on way it's a bit more tricky. Let's put it this way, if an element double it's font-size, it doesn't imply that the space between their baseline and the bottom of the line-height has doubled, in fact a bigger font-size can even make this value change sign.

Relative shift computations

Every elements need to be vertically translated, yes: from the space between their baseline and the bottom of the line-height-area. We need to look a bit closer at the invisible boxes of web typography. There is this beautiful article about css font metrics that introduces the difference between the content-area and the line-heiht-area. This concept allow us to understand why adding an element with smaller font-size but with same line-height can paradoxically increase the height of the line-height-area (usually like footnotes indices).

The shift between the bottom of the content-area and bottom of the line-height-area
The shift from Constant vertical rythm and a Base-aligned rythm is the one between the bottom of the content-area and bottom of the line-height-area for every elements.

The content-area from ascender to descender of the font is vertically centered in the line-height-area. So we have two even spaces at the top and bottom of our line separating the two. The property of centering the content-area in the line-height-area is caused by the default vertical-align: baseline; declared on our block element containing the text (here a p). What this does specifically, is that it first aligns every content-area between them by their baseline, then center the group containing all those content-areas vertically in the middle of the line-height-area.

We can now express our translation value of each element as the sum of:

  1. the space between the baseline and the bottom of the content-area.
  2. the space between the bottom of the content-area and the bottom of the line-height-area..

The first shift value only depends of the font itself, has shown in the previous figure. This content-area depends on the font-metric of our font, and can not be changed through CSS. Note that it actually is the descender of the font, from the baseline and the bottom, so this value can be expressed in em.

The second shift value can be computed as (line-height - content-area-height) / 2. Which depends of both the font-metrics, and the line-height that we set up in CSS. But because content-area-height is the sum of ascender and descender of the font, we can express it as (line-height - (asc / desc)) / 2.

By giving a name to this special value rel_bl = (asc - desc), the whole shift value could be expressed as

  desc + ((line-height - (desc + asc)) / 2) 
= (line-height - (asc - desc)) / 2
= (line-height - rel_bl) / 2

The point is that we don't really care about a more specific expression for rel_bl, as it only depends of the font-metrics, so we know it can be expressed in em. For the same reason this value is unique per font, whatever font-size or line-height we give to each elements. So finding it manually become quite easy by editing through the inspector.

Implementation of Base-aligned rythm

This implementation has to follow the one of Constant vertical rythm. It works by shifting every single block contained in our Base-aligned rythm class from a value that is relative to both the font's metrics and the fixed height unit (defined by --lh). For every different font we have to manually find the --rel-bl which is the part relative to the font's metrics, this can easily be done by setting up the document then increasing and decreasing the value in the inspector until it matches the vertical grid, also drawn on the page with CSS. The elements are vertically translated using position: relative; and a top value, to ensure their translation doesn't move the initial position in the flow of any of the others.

:root{
    --font-1: 'Junicode';
    --rel-bl_font-1: 0.6em;

    --font-2: 'Liberation';
    --rel-bl_font-2: 0.7em;
}

/* GLOBAL */
.ba :is(h1,h2,p){
    /* by default everything as a total lh equal to lh */
    --lh-total: var(--lh);
    line-height: var(--lh-total);

    /* by default everything as a rel-bl of the main font */
    --rel-bl: var(--rel-bl_font-1);
    /* and is in this font */
    font-family: var(--font-1);
}

/* EXCEPTIONS */
.ba :is(h1,h2){
    /* those changed font */
    font-family: var(--font-2);
    /* thus must change their --rel-bl value */
    --rel-bl: var(--rel-bl_font-2);
}

.ba h1{
    /* those double their line-height */
    --lh-total: calc(var(--lh) * 2);
}

/* ALIGNMENT */
.ba :is(h1,h2,p){
    position: relative;
    top: calc(calc(var(--lh-total) - var(--rel-bl))/ 2);
}
A two-columns example of a Base-aligned rythm layout
The example explained in the code snipped above, of a Base-aligned rythm layout with two fonts. The headers are set in Liberation Bold, which has different metrics than the Junicode.

Because the --rel-bl value is relative to font's metrics it is possible that you have to change it for a bold or italic version of a font, depending of it's drawing. On a document with two fonts as show in the example, once those values are found, we can style every elements as we want and we don't have to adapt any of those values anymore and everything will stay in place griddier than ever, whatever the typographic styles.

Rythms and their grains

By using relative positionning on every element — desynchronising every individual boxes of the native flow of HTML — it is clear that the above methodology goes quite against The Web’s Grain

The web is forcing our hands. And this is fine! Many sites will share design solutions, because we’re using the same materials. The consistencies establish best practices; they are proof of design patterns that play off of the needs of a common medium, and not evidence of a visual monoculture.

For printed material made out of HTML & CSS, the akward zigzag of the code is no more present after printing, and what we see is the baseline of the text, while their weird boxes become totally invisible artefact of the craft. We could say that we finally have here a nice solution to make the DIY practice of CSS-based printed layout benefit from the classic elegance of the academic typographic rules.

However, by becoming it's own culture of alternative to Adobe and proprietary softwares, the web to print practices has inevitably developped it's own grain. It is clearly revealed to me when I can see the artefact of CSS printed on paper: some words circled by colorfull ellipsis whose dimension where entierly defined by the length of the contained word recognising a border-radius: 50%;, or the rawness of spacing andabsence of typographic adjustment, or some quite organic and random display: flex or punk-ish linear-gradient. Those type of process afterfacts can trigger some kind of childlike expression on my face because they delicately unveil the singularities how the tool used. And because web2print is part of a somewhat ideological mouvement trying to take another path from the hegemonic way of thinking print layout (imposed by the closed workflow of adobe), all those typographic artefacts becomes the aesthetic of a counter-culture.

Eigenschriften, Irma Blank, 1968.

For me as a designer, the pleasure of investigating those questions where certainly not about stating what is more elegant, or trying to fill a hole in something: it is about technical curiosity and exploring different patterns and rythms and learning about the way they feel. In the end this text is, once again, about opening the boxes, and by doing so opening the choices. In hope that you can wonder the right position at the right time between the convenience of simplicity and the precision of constructions.