A Tale of CSS and Sass Precision

Kitty Giraudel
Share
Various browsers as totems looking at a mysterious percentage sign

Artwork by SitePoint/Natalia Balska

At Edenspiekermann, we rely heavily on code reviews to make sure the work we commit is good enough™. One thing I regularly come across is the fuzziness around numbers, especially the ones that have a decimal point. Hence, here is a short article to shed light on this matter.

Initial Setup

To make the whole explanation clearer before even getting started, we are going to work on a small piece of code that happens to be very relevant to our case.

.list-item {
  float: left;
  width: 33%;
}

What’s the Problem?

Maybe you are wondering what the problem is with this code snippet. In appearance, not much. It is a three-column grid layout. Quite the usual you could say.

Although, 33% + 33% + 33% equals 99%, not 100%. While it might not make any difference in most cases, when working on straight alignments — 1% can make a big difference. 1% of a 1400px large container is 14px. That’s a pretty big distance.

Why don’t we just move the decimal dot (or rather add it) to make it more precise? We could probably reduce the gap to 1.4px, or even 0.14px which is not worth bothering anymore I suppose! Let’s start with that then.

.list-item {
  float: left;
  width: 33.33%;
}

That works better but it still ain’t perfect. This problem has been extensively discussed in this absolutely great article from John Albin Wilkins entitled “Responsive Design’s Dirty Little Secret”. If you haven’t read it, read it.

Can’t the Browser Handle This?

At this point, you might be wondering why the browser cannot just make it work™. The thing is, the CSS specifications don’t specify (of the irony) anything to browser vendors about what to do in case of floating precision with percentage numbers. And when the CSS specifications omit a detail, you can be sure that every browser will do it its own way.

Take this example from the aforementioned article:

[…] with a 6 column grid, each column is 100% ÷ 6 = 16.666667% wide. On a 1000 pixel wide viewport (which I’ve conveniently picked to make our math easier), that calculates to 166.66667 pixels per column. Since the spec gives no guidelines, browser vendors are free to make their own rules. If a browser were to round to the nearest pixel, in our example, we’d arrive at 167 pixels. But since 167 x 6 = 1002 pixels, we’d no longer have room for all 6 columns in our viewport. Alternatively, if a browser rounded down to 166 pixels per column, we’d be 4 pixels short of perfectly fitting all columns into our viewport.
John Albin Wilkins

That’s exactly what happens. Old versions of Internet Explorer (mainly 6 and 7) round to the closest whole number, resulting in layout breakage. WebKit browsers round down, which prevents any catastrophic layout result but leaves us with extra space. Opera (at least in its old rendering engine) was doing some weird stuff that I won’t even bother to explain. But again, there is no rule about this behaviour in the spec, so who’s to blame? Not the browsers resorting in subpixel rendering, that’s for sure, because in the end that’s what gives the best results.

Anyway, it’s pretty much a mess, and we’ll come back to this in the conclusion of this article.

What About Sass?

Sass supports mathematical operations. It is not new and is actually one of the first few things Sass was used for (to build math-based grid systems). What we could do is tell Sass that we want to divide our container’s width in 3 equal parts.

.list-item {
  float: left;
  width: (100% / 3);
}

We could also use the percentage(..) function for the same result:

.list-item {
  float: left;
  width: percentage(1 / 3);
}

Sass, in both Ruby and LibSass, has a precision option of 5. That’s actually a problem because it is pretty low; 10 would be better but that’s not the default (although configurable, but not in an easy way).

This code will yield the following CSS:

.list-item {
  float: left;
  width: 33.33333%;
}

That does not solve our browser problem, but that does make authoring stylesheets easier. Not only do we not have to handle the calculation and the precision ourselves, but we also make the code more convenient to read and update by actually displaying the calculation.

I’d say that is a good thing.

The Best of Both Worlds

So far, we have learnt that it is good to let Sass handle the computation for us rather than hard coding the value. Now, the best approach would be to let the browser handle this in the best way it can. For this, there is the calc(..) CSS function.

.list-item {
  float: left;
  width: calc(100% / 3);
}

This code snippet does not get compiled into anything. It hits the browser as authored. Then, it is up to the browser to make the best of it. I’ll be entirely honest with you and tell you I’m not sure whether browsers handle calc(..) values the same as regular ones. I suppose they perform the calculation, then do their rounding. Some browsers appear to bring subpixel rendering into the equation. If you have any insight on this, please share in the comments.

For browsers that do not support the calc(..) expression, mostly Internet Explorer 8 and Opera Mini, we can put a static value expressed as a Sass operation right before it. This way, we get the best of both worlds.

.list-item {
  float: left;
  width: (100% / 3);
  width: calc(100% / 3);
}

Conclusion

Let’s have a little recap. For starters, percentage based layouts are hard to handle because of browser inconsistencies and a lack of specification in the matter of floating precision.

Then, hard-coding values resulting from a somehow complex calculation is usually not a good idea. We can let Sass compute an approximation (to 5 digits after the floating point).

Even better, we can let the browser compute an approximation. In a perfect world, when the browser is in charge of both the math and the rendering, it should be able to make the best out of it. To head in that direction, we rely on the calc(..) function.

That’s pretty much where it is at right now. Nothing new, but I thought a quick recap would help!