Title: EDS 2.0 Typography and Spacing
Author: Victor Nystad <vnys@equinor.com>
Staus: Work in progress
Date: Dec 08 2022
- A harmonious typographic scale based on the classic scale used in print for centuries
- Line-heights that are rounded up to the nearest 4px grid-line
- Spacing which uses a combination of line-height and vertical padding so that base-lines aligns with grid-lines
Based on the classical typographic scale from the movable type era. Best described by Spencer Mortensen in his article “The Typographic Scale”, where the typographic scale is based on the pentatonic (five-tone) musicale scale, so the next step after the base font-size can be calculated using the formula
- f0 is the fundamental frequency, or 16px.
- r is the ratio, or 2
- i is the step in the scale, for example 1 for the next step above f0, or -1 for the next step below f0
- n is the number of notes in the scale, or 5
So to find the next step after 16px, the formula is
- f-4 = 9px
- f-3 = 11px
- f-2 = 12px
- f-1 = 14px
- f0 = 16px
- f1 = 18px
- f2 = 21px
- f3 = 24px
In our typography tokens step 3 corresponds to the fundamental frequency, for historical reasons when font-size 3 was the default font-size in the browser.
One obvious problem with that is the increment between 9 and 11, and 11 and 12 – where there are two pixels between the two smallest values, and then just one pixel between the second and third smallest. A better solution then, would be to round up to the nearest 0.5.
That would give us the following modified scale, with a much smoother transition from one step to the next:
- f-4 = 9px
- f-3 = 10.5px
- f-2 = 12px
- f-1 = 14px
- f0 = 16px
- f1 = 18.5px
- f2 = 21px
- f3 = 24.5px
In the Figma Tokens plugin the formula for f1
looks like this:
5*roundTo((16*2^(1/5)/5),1)
… or using aliases:
5*roundTo(({eds.core.const.f0}*{eds.core.const.r}^(1/{eds.core.const.n})/5),1)
In the EDS we use a 4px hard grid. That means that instead of using spacing values in-between invisible bounding boxes, we use space between base-lines. That also means that we try to align the base-lines in a text-frame with the grid-lines in a layout-grid, with some exceptions where it makes more sense to vertically align a line of text to center, for example in a button component. The first step in achieving this, is rounding the line-height up to the nearest grid-line. We have four line-height scales with various density:
Density | Value |
---|---|
Tight | 100% |
Compressed | 110% |
Comfortable | 125% |
Relaxed | 150% |
Relaxed is WCAG compliant, and is used for body copy. As the font-size increases with the header levels, the
Using fontSize2 (14px) and relaxed (150%) as an example, simply using a percentage would give us a line-height of 21px, which does not align with the layout grid. 24px, however, does. In order to achieve that, we need to use the following calculation in the token:
4 * ceil(14 * 1.5 / 4)
… or using aliases:
{eds.core.const.grid} * ceil({eds.core.fontSize.3} * {eds.core.const.relaxed} / {eds.core.const.base})
There are some overlapping numbers in the scales, for example, the line-heights for fontSize3 are as follows:
Density | Value |
---|---|
tight3 | 16px |
compressed3 | 20px |
comfortable3 | 20px |
relaxed3 | 24px |
However, from a user perspective we believe the advantage of using the scales outweighs the increase in number of tokens.
Heads up! The following calculations work perfectly in Figma, but not in browsers due to how line-height is calculated, and have to be changed. Working in Codepen at the moment, and will update this document once it’s solved.
In addition to using a hard grid in the EDS, we also use optical margins. That means that in some cases where the optical margin is a certain value, we might have to use a number that seems out of place to arrive at the optimal result.
One of the reasons that this is sometimes necessary, is that Figma does not have support for negative padding or leading-trim. The consequence of that is that if you have a textframe where the base-lines are aligned to the layout-grid, and you increase the line-height, the lines of text will be moved down vertically, and so the top padding has to be decreased so that the first line stays in place.
This may seem like a lot of work for components that only have one line of text, but even though we recommend that buttons for example shouldn’t have multiple lines of text, we should still ensure that the components don’t break for the product teams who choose to ignore our recommendation. That also means that none of our components that contain textframes have fixed heights.
The calculation for the vertical padding in combination with the line-height requires us to know the actual cap-height of the font. Using FontForge, we can se that the height of 1em is 1000, and the cap-height is 700 – or in other words, the cap-height is 70% of 1em, which is an easy value to work with. That means that in order to have a line of text that has no vertical space, we can set the line-height to 70%.
For anyone else wanting to try out the tokens who don’t have access to the Equinor fontfiles, Inter has an em-height of 2816, and a cap-height of 2048, or 72,7272%.
So let’s say we have an autolayout container with a textframe with a 14px font-size and a 20px line-height, where the calculated height of the container is 36px, this is how we find the top and bottom padding needed to ensure that the base-line aligns with the layout grid.
Property | Value |
---|---|
Font-size | 14px |
Line-height | 20px |
Cap-height | 0.7em |
Rounded cap-height | 14 * 0.7 = 9.8 ≈ 10 |
Nearest grid | 10 ≈ 12 |
Optical vertical spacing | (36 - 12) / 2 = 12 |
Average vertical padding | (20 - 10) / 2 = 5 |
Padding bottom | 12 - 5 = 7 |
Padding top | 36 - 20 - 7 = 9 |
📝 TODO: Replace Google Sheets table with formula.
This document is work in progress, check out a simplified copy of the tokens repo which only deals with spacing and typography at https://github.com/vnys/token-scripts