Setting up Theme UI for Vertical Rhythm

I’m trying out both vertical rhythm and Theme UI for the first time with this site. Here’s how I’m putting them together.

We’re going to take advantage of the “scales” feature that Theme UI inherits from Styled System. This lets you define, say, a range of font sizes or spacing amounts, and then reference them by index.

Here’s an example, adapted from the Funk preset:

{
  fontSizes: [ 12, 14, 16, 20, 24, 32, 48, 64, 96 ],
  lineHeights: {
    body: 1.625,
    heading: 1.2,
  },
  styles: {
    h1: {
      fontSize: 5,           // 32px
      lineHeight: "heading", // 1.2
    },
    …
  }
}

When we want to enforce vertical rhythm, however, it becomes important to tie font size and line height together, so that the two combine to a multiple of the theme’s baseline. Let’s look at how we do that.

Picking a Baseline

First, you’ll need to pick what you want your baseline to be for the vertical rhythm. This is typically going to be the overall height of a line of body text, as well as a common denominator in your between-block spacing.

I chose a body text size of 16px and a line height of 1.5, so my baseline is 24px. Let’s take advantage of the “JS” part of “CSS-in-JS” and save that out as a constant, because it will come up a lot.

const BASE_FONT = 16;
const BASE_LINE_HEIGHT = 1.5;
const BASELINE = BASE_FONT * BASE_LINE_HEIGHT;

Calculating Font Sizes and Line Heights

As I mentioned in my last vertical rhythm post, I used the Modular Scale calculator to choose a “harmonious” range of font sizes. My choice for ratio was 1.414 (“augmented 4th / diminished 5th”).

Using a bit of exponent math, we can generate our font sizes:

const RATIO = 1.414;

// [ 16, 23, 32, 45, 64, 90 ]
const FONT_SIZES = [0, 1, 2, 3, 4, 5].map((n) =>
  Math.round(BASE_FONT * RATIO ** n)
);

Given that array of font sizes, we can calculate the line-height for each size that will put it on a multiple of our baseline:

// [ 1.5, 1.0435, 1.5, 1.0667, 1.125, 1.0667 ]
const LINE_HEIGHTS = FONT_SIZES.map(
  (f) => (Math.ceil(f / BASELINE) * BASELINE) / f
);

The javascript›Math.ceil(…) part finds the next biggest multiple that will fit the given font size. For example, javascript›Math.ceil(45 / 24) === 2, so for a 45px font size we’d target fiting in to a baseline multiple of 2 (i.e. 48).

Setting up the Theme

We now have a calculated a paired of arrays: javascript›FONT_SIZES and javascript›LINE_HEIGHTS. For every font size we have a matching line height that will maintain the vertical rhythm. That makes them a perfect fit for Theme UI scales. All we need to do is be consistent with our values for javascript›fontSize and javascript›lineHeight:

{
  fontSizes: FONT_SIZES,
  lineHeights: LINE_HEIGHTS,

  styles: {
    root: {
      fontSize: 0,
      lineHeight: 0,
    },
    h1: {
      fontSize: 5,
      lineHeight: 5,
    },
    …
  }
}

Factoring Out Responsive Presets

The last step you can take is factoring out the paired javascript›fontSize / javascript›lineHeight properties into helper “presets.” I used this so that I could be consistent with responsive settings for logical text sizes.

export const TEXT_MEGA = {
  // 45px below 768px
  // 64px between 768px–960px
  // 90px above 960px
  fontSize: [3, 4, 5],
  lineHeight: [3, 4, 5],
};

{
  breakpoints: ['768px', '960px'],

  fontSizes: FONT_SIZES,
  lineHeights: LINE_HEIGHTS,

  styles: {
    h1: {
      ...TEXT_MEGA,
    },
    …
  }
}

There are a few things to note about using the spread operator with themes:

  • Theme UI has support for “variants” to group styles together, though you can only apply a single variant to a style or element at a time. This makes them a better fit for making distinct kinds of a particular element (“primary button” being a common example) rather than utility styles that you’d be combining several of (such as “large text” and “inverted color scheme”).

  • JavaScript object spreads do not have the “deep merge” behavior that is common when thinking about styles. This is not a problem if you’re doing simple properties that you want to override, but is confusing if you’re mixing in nested styles:

      const A_UNDERLINE_ON_HOVER = {
        a: { textDecoration: 'none' },
        'a:hover': {textDecoration: 'underline'},
      },
    
      {
        styles: {
          h1: {
            color: 'primary',
            a: { color: 'inherit' },
    
            // This wipes out the "color: inherit" rule
            ...A_UNDERLINE_ON_HOVER,
          },
        },
      }
    

All done!

You can see this code in action in the site-theme.ts file in this blog’s theme.

Was this useful? Is there something cool I’m missing about Theme UI? Drop me an @-mention or DM on Twitter: @fionawhim