Implementing baseline rhythm in CSS
In this guest post from Pilot engineer Jan Dudek, he discusses how designers can make their work look harmonious and clean by aligning type to a vertical grid. By correctly implementing the same visual rhythm, front-end architects can also achieve consistent, good-looking results more quickly and easily.
Jan Dudek
PEO vs EOR: What are the Differences and Which is Right for You?
Read Story →A Guide to the Best Remote Work Tools for HR Teams
Read Story →How to Manage a Remote Team: 6 Ideas for Better Remote Work
Read Story →Achieve clean and consistent work without the need for designer input in the process.In this article, I’ll help you lay the foundation for the proper implementation of a vertical rhythm in CSS. Let’s start by clarifying what we won’t be working on.This concept has been present on the web for years. It introduces a common line height value (or its multiple) that’s used for all elements, including their paddings and margins, occasionally taking border widths into the equation.In that scenario, according to the CSS standard, the type gets vertically aligned in the middle of two grid lines. But insert two differently formatted elements next to each other and they’ll seem out of phase.There is an alternative approach that provides consistent results regardless of the font settings used. That’s aligning the baseline of the text. By using this technique, all type — regardless of its size — lies on the same grid line.CSS doesn’t provide any handy tools for that, so we’ll have to make them manually using a few tweaks. There are two things to find out:
- How far the content has to be shifted.
- How to efficiently shift it by that amount.
Determining the shift
Razvan Onofrei wrote an excellent article that explains this part.In short, the height of a capital letter above the baseline is called the cap height (and that’s what browsers will center out between the grid lines automatically). What we need to do is to shift it by half the difference between line height and cap height.A cap height is a property of the font that’s being used. It can be determined experimentally by fiddling with the values until our type is properly aligned with the grid.Here’s an excerpt from our stylesheets based on the idea:$line-height: 24px;
$font-stacks: (
s: $font-stack-text,
m: $font-stack-text,
l: $font-stack-display,
xl: $font-stack-display
);
$font-sizes: (s: 13px, m: 15px, l: 19px, xl: 27px);
$cap-heights: (s: 0.8, m: 0.8, l: 0.68, xl: 0.68);
// Accepts s, m, l, or xl
@function rhythm-shift($size-name) {
$font-size: map-get($font-sizes, $size-name);
$cap-height: map-get($cap-heights, $size-name);
$offset: ($line-height - $cap-height * $font-size) / 2;
@return round($offset);
}
Applying the offset
Once we know how far the text should be shifted, we need to decide how to do it reliably. Retrospectively, we’ve approached it in a few ways.Solution 1. Take advantage of relative positioning.
Use thetop
property to shift the content without affecting the context.$offset: rhythm-shift(m);
.rhythm-m {
position: relative;
top: $offset;
}
👉 See this example.This might be the easiest option, but you may encounter at least two problems with this approach along the way:1. position: relative
affects the stacking of elements. If two elements overlap, the one positioned relatively is displayed on top. At some point, this might involve undesirable manual z-index
tweaks.2. position
may be needed for different purposes, e.g., for absolutely positioned content.This solution surely works great when the codebase feels small and simple. We eventually ruled it out, though, when our app became more complex and the architecture required more scalability.Solution 2. Use positive top padding and negative bottom margin.
This is the approach suggested in the article I mentioned:$offset: rhythm-shift(m);
.rhythm-m {
padding-top: $offset;
margin-bottom: -1 * $offset;
}
👉 See this example.The top padding shifts the content just as we need. The negative bottom margin is used to compensate for this offset. It’s important to always use margins in one direction only, e.g.. only bottom margins. Otherwise they collapse and break the whole system.The biggest drawback of this solution is how it quickly adds complexity. Take the case from our app, where you’ll find utility padding classes: pt-1
adding 24px top padding, pt-2
48px (two line heights), and so on. Using these classes together either requires additional HTML containers or results in overuse of Sass features for generating all possible cases. Both cases make the work cumbersome and impossible to phase out easily later on.Solution 3. Use positive top margin and negative bottom margin.
This is the final solution that we ended up with:$offset: rhythm-shift(m);
.rhythm-m {
margin-top: $offset;
margin-bottom: -1 * $offset;
}
👉 See this example.As in the previous approach, the top value — margin this time — is compensated with the negative bottom margin.What about collapsing margins?
Fortunately, there’s a neat trick that bypasses this issue. But first, let me remind you how collapsing works in the presence of positive and negative margins:- Given two positive margins, the bigger one wins. For
margin-bottom: 30px
and thenmargin-top: 20px
, the final space between these elements is 30px. - Given two negative margins, again, the lower (meaning the more negative one) wins.
- Given a positive and a negative margin, they sum up. For
margin-bottom: 30px
andmargin-top: 20px
, they result in 10px of whitespace.
Overcoming margin overflow
Margins in CSS have one more nasty feature: if an element doesn’t have a border or padding, and its first child has a margin, that margin will flow out of the parent. This becomes an issue when the parent has a background. That background will start wherever the child appears, and not within the parent.There are two ways to go about it.- Either use
overflow: hidden
to force the margins to be contained within the parent, - or add
padding-top: 0.1px
, which is a small hack. The value is too small to be actually rendered, but it’s enough to keep the child within the parent container bounds.