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.

Book called stunning CSS on top of JS and JQuery book

Jan Dudek

Published on June 15, 2016

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:
  1. How far the content has to be shifted.
  2. 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 the top 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 then margin-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 and margin-top: 20px, they result in 10px of whitespace.
The last point means that if we always alternate positive and negative margins, their values will be summed and our type will stay aligned to the grid lines.To maintain this guarantee, we decided not to use margins anywhere in our app with the exception of the rhythm system. It’s not a big loss, though — uncurated margins aren’t always predictable anyway.

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.
👉 See this final example.---That’s it. We’ve been successfully using this system in our app for months now.Even though CSS doesn’t provide such a system out of the box, laying the right foundations for our framework made it perfectly achievable.

Related articles

From startups to large corporations, US companies of all sizes use Pilot for international payroll, benefits and compliance.

Want industry news and product updates?Sign up for our newsletter.