Enforcing Design Conventions in Code: When Both Engineers and Designers Win

It’s common to think of design and engineering as separate disciplines. In many ways, they’re distinct — the nouns and verbs they use to map their respective domains are quite different — but there’s a larger area of overlap than people think. I want to share one of those areas with you, with some code to help you make your designers happier and to simplify your own life working in the UI code.

A few months ago we did a typographic revamp on our site. Some of that work was in the user-facing type, where we increased the base font size, adjusted kerning and worked on our headings. Much more of it was under the hood. The code problems we were trying to tackle looked like this:

.my_random_class_that_only_appears_once { font-size: 13px; }

What’s up with that font size? Why is it 13px? That’s only one pixel off from our base font size! Is it really necessary? What did the author of that code really want? And more importantly, how can we work with that the next time we do a redesign?

Yo dawg, I heard you like font sizesOur CSS was just littered with examples like this. I found the following font size declarations, all in pixels: 11, 12, 13, 14, 15, 16, 18, 20, 21, 22, 24, 25, 26, 28, 30, 32, 36 and 48. What a mess! How we got there is very clear, though. Over the years, we never had strong design direction for our typography. We had a variety of developers working on building out pages, making things look however they thought would be best. So we put ourselves in the situation of having code that was hard to work with, and a very fragmented visual experience on the site.

It was time for a change.

Easy decisions

I like easy decisions. As a developer, day-to-day, I shouldn’t be wrestling with what fonts to use. I should have a very, very short list and be able to choose from them, with rules or guidelines to aid my decision. I like not having to worry about how much vertical space I should put after a paragraph. Let me focus on what matters, not on those kinds of details. Of course, those details are definitely important; if we didn’t have consistency in those decisions, our site would look like some terrible MySpace refugee. But I shouldn’t have to worry about them when I’m building out new functionality. I should be worrying about the layout, the data and the UX.

When we tackled the typography rework, we started by choosing a suitable font. We went with Helvetica Neue and Helvetica Neue Condensed, since that was what our marketing team was using at the time. That was an easy decision. I found myself wishing that all of our font-related decisions could be that easy!

Fortunately, they can be.

The design of a single piece, like a concert poster or a greeting card, can give you a lot of freedom. You can choose to have systems or not — it’s up to you. But when you’re working on a larger work, like a book or a magazine, you need consistency so that your readers aren’t trying to figure out why you switched typeface mid sentence, and why this paragraph is a different color than that one. A website is the same. You need systems to give the site a unified look and feel.

There is a wide world of information out there about building good systems. Good design is having good systems; great design is knowing when and how to break them. In our particular case with font sizes, we needed to establish a system first.

Modular scale

With font sizes, it’s easy to build a system. You just need to decide what your modular scale should be. It’s an easy concept: you start with a base font size, and then choose a multiplier. All font sizes in your system are products of the base font size and the multiplier. For example, if your base font size is 16px, and you choose a multiplier of 1.5, then your font sizes might be 16px, 16 x 1.5 = 24px, 16 x 1.5 x 1.5 = 36px and 16 x 1.5 x 1.5 x 1.5 = 54px.

A modular scale, like a musical scale, is a prearranged set of harmonious proportions.
—Robert Bringhurst

Let’s take a look at this idea. (It’s best expressed visually, anyway.) Here’s an example of the scale we just built, based on 16px with a ratio of 1.5.

A modular scale for type, with a base size of 16px and a ratio of 1.5.

Now, let’s mess with that a bit. Here’s a very similar scale, but with one of the headings changed just a touch.

A modular scale for type, with a base size of 16px and a ratio of 1.5. The smallest heading has been made too small, though.

Look at those two examples. The first feels much nicer! It’s much more locked-in. In the second example, I dropped the smallest heading’s size down by 4px, from 24px to 20px. Now it just seems out of place; the set of headings no longer has the internal harmony of the first example.

We now have a wonderful set of rules that we’ll want to apply to all of our type. Long live the modular scale!

Computers like rules

This is one of those great areas where the engineers and the designers are in sync. The designers want the type to obey rules, and the engineers want simple rules that they can encode into the system. Let’s see what the rules look like.

We happen to use SCSS with Compass, and that makes it really easy to build up our system. This is our _typography.scss file, which we include everywhere.

@import "compass/typography"; @import "compass/typography/vertical_rhythm"; // Set up the variables required by Compass' vertical rhythm code $base-font-size: 14px; $base-line-height: 18px; $relative-font-sizing: true; $round-to-nearest-half-line: true; $font-unit: if($relative-font-sizing, 1rem, $base-font-size); // Get the font-size for the number of steps above/below body text // Our modular scale uses a ratio of 1.3 between levels @function modular_scale_size($adjustment: 0) { @if $adjustment == -2 { @return 11px; } @else if $adjustment == -1 { @return 12px; } @else if $adjustment == 0 { @return $base-font-size; } @else if $adjustment == 1 { @return 18px; } @else if $adjustment == 2 { @return 21px; } @else if $adjustment == 3 { @return 27px; } @else if $adjustment == 4 { @return 32px; } } // Get the number of $base-line-heights for the number of steps above/below body text // If the font size is too big relative to the number of lines, everything looks cramped. @function modular_scale_lines($adjustment: 0) { @if $adjustment == -2 { @return 1; } @else if $adjustment == -1 { @return 1; } @else if $adjustment == 0 { @return 1; } @else if $adjustment == 1 { @return 2; } @else if $adjustment == 2 { @return 3; } @else if $adjustment == 3 { @return 3; } @else if $adjustment == 4 { @return 3; } } // Use the modular scale to get bigger/smaller fonts than the default. // The $adjustment parameter is 0 for the default font size, or between // -2 and +4 for 2 sizes smaller to 4 sizes bigger than the default. @mixin set_font_size($adjustment: 0) { $new_size: modular_scale_size($adjustment); $new_line_count: modular_scale_lines($adjustment); @include adjust-font-size-to($new_size, $new_line_count); }

That looks like a lot of code, but it’s really not as bad as it seems. First we import Compass, then we lay out some variables that will be helpful, establishing our base font size and a vertical rhythm. Then we get to the meat of it. The modular_scale_size and modular_scale_line functions both work the same way. They take a font scale number, which ranges from -2 to 4. We decided that 0 is the base font size, and it takes up one line height. As you go up the scale, the font sizes scale up by a factor of 1.3, and we give the headings suitable line heights.

Notice that we’re not doing any inline math to figure this out. We could have, but 1.3 is an awkward ratio and we would have ended up with font sizes that were non-integers. Those don’t hold up well in some browsers, so we just did the math by hand and hard-coded the results. But you can easily figure out that 14 x 1.3 = 18.2, so we just rounded a bit. If we’d used 1.5, we could have done the math inline because no rounding would have been necessary. On a blog or magazine site, a ratio of 1.5 would be great. However, for our data-heavy site, a ratio of 1.5 would have been overwhelming, making the headings dwarf the data we’re displaying.

As a developer, all I need to do now is call @include set_font_size(1) in my rule, and I get the correct font size (and line height!) for one step up the modular scale we’ve defined. For example, here are our new heading definitions:

h1 { @include set-font-size(3); font-weight: normal; letter-spacing: -0.05em; // Helvetica looks great when set tighter } h2 { @include set-font-size(2); font-weight: normal; letter-spacing: -0.05em; } h3 { @include set-font-size(1); font-weight: normal; } h4 { @include set-font-size(0); font-weight: bold; }

Those are great! It’s easy to see how we map the scale into our headings. But as engineer, I’m still unsatisfied. There’s nothing preventing anybody from putting a random font-size declaration anywhere in the CSS, leading to the mess we were in before.

Thankfully, we have tests.

“But wait,” you say. “You can’t test CSS!” Generally, you can’t. But for this, we can. Notice that with the mixin, you’ll always have a parenthesis after the string “font-size”, whereas with normal CSS you’ll have a colon. We can use this.

Test all the CSS!

Test all the CSSI wrote a test to assert that we’re all using the correct pattern, all of the time. This doesn’t test if the font sizes are correct — it tests that we’re using the new set-font-size API.

require 'test_helper' class CssModularScaleTest < ActiveSupport::TestCase directories = ['app/assets/stylesheets'] allowed_files = ['chosen.scss', '_typography.scss', 'second_sku.scss'] # First, grab all of the .scss files, rejecting the files we allow to break the rules files = [] directories.each do |dir| files += Dir[File.join(dir, '**', '*.scss')].reject{|file_name| File.directory?(file_name) || allowed_files.any?{|af| file_name.ends_with?(af)} } end files.flatten! # All font-size declarations in our CSS must use the modular scale, defined at the top of _typography.scss. # This test asserts that all of our CSS uses the modular scale. files.each do |filename| context "#{filename}" do should 'only use the modular scale for declaring font sizes' do regexp = /.*?font-size:\s*(.*?);/i # Find statements with the colon! open(filename) do |f| # Look for statements like these: # font-size: 12px # font-size: 3% matches = f.grep(regexp) passing = true matches.each do |match| next unless passing match =~ regexp # Get the capture parameter passing = false unless $1.in?(['100%', 'inherit']) # Sometimes, it's ok to use the raw CSS end flunk "Must use set-font-size(x) instead of font-size: x. font-size: 100% and font-size: inherited are allowed." unless passing end end end end end

It was hard making the test go green the first time. It took several days of searching for “font-size:” declarations, switching them to use the new scale, and visually verifying that the affected pages still looked good. But with this test in place, if any developer checks in an arbitrary font size, the build will fail. We can now ensure that we’re all following the conventions! And there’s no chance of any out-of-place font sizes making it in to production.

The test is also stable across redesigns. If we ever change our base font size, or our modular scale, the test will be fine. The functions in the _typography.scss will need to be changed, but we now have a single lever we can use to control the type site-wide, and the visual relationships between elements will be maintained.

Cultural fallout

Come on!I’ll be honest. This took a little while to get used to. At first, developers resented the test that failed for their CSS. I mean, really. CSS? Come on!

The designers had to adjust as well. They were used to being able to hand-craft every page, lovingly caring for every piece of text. Not anymore.

The developers came around really fast. They really liked not having to make decisions anymore. They also really liked the easy API. The designers took a little bit longer, because their habits were a bit more ingrained, but after a week or two they all adjusted to the brave new world, and also felt the relief at having more constraints. More constraints often make for better design anyway.

The API at work

Once the new API was in place, we got to use it. The days of making the test go green immediately paid off. We were able to play with the modular scale, refresh the page, and immediately see site-wide how the new type system behaved. It allowed the design team to really investigate how the system worked with our data on our pages all at once, without the tedium of trying to do it in Photoshop. That’s how we came up with 1.3 as our ratio — we played with it until it looked right.

The next time we have to do a redesign, we’re all going to be very happy campers.

We’ve had a lot of success at New Relic finding areas like this where design principles and engineering principles overlap. It gives the designers and engineers a shared language, simplifies the code, and makes the fundamentals of the UI easy to understand for our users. Everybody wins.

Brent Miller is a principal engineer & architect for New Relic. He traded in his training as a botanist to become a frontend engineer, and has spent the past decade building UIs that are easy to manage and helping the engineers around him become better at what they do. View posts by .

Interested in writing for New Relic Blog? Send us a pitch!