loading...

How and Why I Avoid Magic Numbers in CSS

mpuckett profile image Michael Puckett ・3 min read

Introduction

There's a great article called Code smells in CSS by Harry Roberts that first introduced me to the concept of magic numbers in CSS.

When Harry wrote the article in 2012, there weren't too many great solutions for dealing with these problems. But since then, we've gotten Grid and Flex layout, along with CSS custom properties (variables!) so we can now avoid many of the pitfalls.

The basic idea of magic numbers is that writing styles with arbitrary numbers like the following is brittle and unmaintainable:

.container {
    height: 250px;
}
.box1 {
    padding-left: 4px;
    position: relative;
    top: 11px;
}
.box2 {
    padding-left: 2px;
    padding-right: 4px;
    position: relative;
    top: 7px;
}

For one thing, what's so special about these numbers? Even if there were comments, you might not know all that they're doing unless you look at the markup, or how they relate to the other numbers.

Let's say .box1 and .box2 are next to each other.

Focusing on .box2, let's first look at its padding -- 2 on the left and 4 on the right. The left padding is defining the space between the components, and the right side in defining the outer right margin of the page. That's not very conducive to composability. What if we wanted to reverse the order? We would have to sift out the 2 and 4, reverse them, and apply them to the other component. It would be better if the container could manage these details.

The 11px and 7px top values here are vertically centering the boxes inside their container. That's precarious, in case the content changes. There are better ways to achieve this.

Grid Layout

In the example above, with one padding defining the space between components, and the other defining the page margin, we can leave all those details to the grid container. The container gets the following:

.container {
    display: grid;
    padding: 0 4px;
    grid-gap: 2px;
    align-items: center;
    grid-auto-flow: column;
    height: 250px;
}

Now the interior components don't need padding.

Notice also how Grid supports align- and justify- properties that can do the work of centering and aligning, so we can lose the 11px and 7px top value altogether.

(Flexbox also has these alignment properties, but the support for gap is still limited, unfortunately.)

Custom Properties

Any time that we still need to use specific numbers, we can replace them with CSS custom properties: --page-padding: 4px and --gap-width: 2px

Note: I think it's fair to be cautious with a statement like this, and to be conservative when applying this rule in practice. There are potential performance considerations when using CSS custom properties all over the place. But in my experience, the bottleneck in rendering a page hasn't been the CSS.

In keeping with this, we can keep the 250px height as-is, because it's unique.

Similarly, --page-padding might be a better candidate for being a custom property than --gap-width, for example. If the gap between the components is unique, keeping it 2px might be fine.

But what if, when it was originally conceived by the designer, the space between the components was designed to be half of the page padding? If so, you might want to preserve the relationship in case the page padding changes in the future.

You can use calc for this purpose:

.container {
    display: grid;
    padding: 0 var(--page-padding);
    grid-gap: calc(var(--page-padding) / 2);
    align-items: center;
    grid-auto-flow: column;
    height: 250px;
}

Now we're down to just one magic number, and one appropriately-named custom property. In my opinion, that's much easier to reason about and the intent of the code is clearer.

I found a similar article on Dev that goes even more in depth if you want to check it out: https://dev.to/adrianbdesigns/mastering-css-magic-numbers-2297

✌️

Discussion

markdown guide
 

I've actually run into this situation myself before, and I have to disagree. This is one of those cases where refactoring actually makes things worse. Not every instance of a magic number is necessarily a code smell.

I would much prefer to see 4px and 2px all over the place in your code instead of --page-padding and --gap-width. Why? Two key reasons:

  1. It's easier to immediately parse. I don't have to track down those properties and remind myself of what they do, and then mentally substitute them everywhere they're used.

  2. Any time you run into a situation where many other things need 8px, 16px, and so on, you'll find yourself defining really obtuse and illegible calcs that try to manipulate page-padding or whatever other custom properties you're using.

The one notable downside to using raw/magic dimensions is that if you decide to make things more cluttered or more spread out in the future, you'll have to replace every instance of those numbers with the updated versions. But that is not something you should run into frequently (if it is, then you haven't devoted enough time to wireframing).

You say:

that's much easier to reason about and the intent of the code is clearer.

But how?

padding: 0 var(--page-padding);
grid-gap: calc(var(--page-padding) / 2);

I look at this and think "Huh, okay, now what's page padding?" A little repetition won't hurt your code. In fact, compare that to this:

padding: 0 4px;
grid-gap: 2px;

Much simpler and easier to read.

This scenario is analogous to the one of unnecessary inheritance in OOP development. The problem you usually run into there is trying to extend something for reuse (to avoid code duplication) where it simply isn't appropriate to do so. You usually end up shooting yourself in the foot.

 

That’s a really great alternative take! Agree that it can go the opposite direction and hinder readability if you go overboard.

Instead of creating multiple calc functions for various paddings/gaps in the code, I prefer having a standardized set of padding variables. That usually keeps things simple, but requires buy-in from the designer.

In the article I linked there’s a better use for calc, something like: calc(var(--standard-padding) - 1px) for rare exceptions.

Also it may be true that upon initial glance you know fewer specifics about the code (“exactly which number pixels do I push?”), the original intent has been translated into the code, which should make the overall process of making changes easier to reason about.

Appreciate your response!