Separation, Abstraction, and Cascading in CSS

Eric Normand's Newsletter
Software design, functional programming, and software engineering practices
Over 5,000 subscribers

Summary: LESS and Sass (and similar solutions) have saved CSS for three reasons: separation, abstraction, and cascading. While I welcome them, CSS still has other problems which I believe can be solved. I propose some solutions.

Introduction

A lot is said about LESS and Sass, and for good reason. CSS is hell to get right and even harder to maintain. LESS and Sass (and similar tools) make CSS into a much more useful language.

But when people talk about why they are so great, they miss the main point. It is true that your style files are now shorter and more readable. However, there is something deeper going on than mere saving of keystrokes and being able to name things.

In this essay, I will try to put into words (and some pictures) what my intuition tells me as a developer and programming language enthusiast to clarify why CSS is innately unmaintainable, does not satisfy its own design goals, and why LESS and Sass make a bad language more bearable. I also will propose solutions which would raise the bar past the high level where LESS and Sass have taken it.

Zero Degrees of Separation

Way back when, people used HTML tables to style their pages. Documents looked like this:

HTML containing content +
style

Those were the days of font tags and tables.

Then CSS came along, and people talked a lot about separation of content from presentation. CSS did help you move styling concerns outside of the HTML file, but that is about it.

HTML containing content and CSS containing
style

Your styles were still tied to the structure of the document they were styling. They had no grouping of their own. If you wanted to repeat a style, you either had to copy and paste or use a selector with a comma. Both were bad solutions.

An even worse solution, which is, unfortunately the most common, is to build CSS classes which name a style. We see this in the numerous and all equally bad "CSS frameworks" which litter your HTML with style information. Grid systems do this to a fault.

But it is not the fault of the authors of those frameworks, nor of the poor web developers who are in search of some solutions to their problems. No, the blame lies with the authors of CSS itself. With CSS, separation of content from presentation is possible but extremely difficult and time-consuming.

HTML and CSS are separate but not equal. HTML can exist without CSS. But what is CSS without HTML? Nothing.

If you truly want to be able to separate content from presentation, you have to set them both on equal footing, like this:

HTML, CSS, plus some third language to tie them
together

Content in the HTML, style in the CSS, and you tie them together in some third language.

This is possible in LESS (LESS being the question mark), evidenced by the existence of frameworks such as LESS Elements. People talk about LESS reducing boilerplate and repetition. Or about hiding browser-specific CSS properties. But that is not the essence of the matter. What all of that talk is trying to get at is that they can finally define a style, in the form of a mixin, which exists independently of any HTML structure. It can then be tied into zero, one, or more HTML elements merely by mentioning its name.

With LESS, I can define a mixin called Dorothy:

.dorothy() {
  background-color: green;
  text-color: yellow;
  border: 1px solid red;
}

Yes, it is probably an ugly style. But it is just a style. It does not depend on any HTML structure for its existence, not even one <p> tag. Now, if I want to use it, I can use it wherever I want by relating, in a separate way, the style with some HTML.

div.main {
  .dorothy;
}

blockquote {
  .dorothy;
}

This is one of the reasons LESS makes styling HTML bearable. In addition to mixins, you can also define variables which contain sizes and colors, which is just another way to name styles (or elements of styles) to be tied to HTML later.

HTML, LESS mixins, and LESS
rules

Abstraction

If you take the idea of mixins and variables even further, you will notice that they compose. I can define a mixin and use it in another mixin. I could define dorothy as the composition of three styles, red-border, yellow-text, and green-background. This type of composition suggests that there is some amount of abstraction going on.

This was not possible in HTML + CSS.

Well, I say not possible, but there were ways, they were just terrible. You could copy-paste, which is just not a solution at all, but it would get you your style. Or you could reuse non-semantic class names like in grid frameworks (blech!). Or, finally, you could do what I call "inverted-style", where the styles take precedence and the selectors take a subordinate role. That will take some explaining.

Let us say we want div.main and blockquote to be styled like dorothy. Also, div.main and p should have a top margin.

Normally, we would write this in CSS:

div.main {
  background-color: green;
  text-color: yellow;
  border: 1px solid red;
  margin-top: 10px;
}

blockquote {
  background-color: green;
  text-color: yellow;
  border: 1px solid red;
}

p {
  margin-top: 10px;
}

This does not look bad, but there is a lot of repetition and the intent is not clear. We could instead write it in inverted-style.

/* Dorothy style */
div.main,
blockquote {
  background-color: green;
  text-color: yellow;
  border: 1px solid red;
}

/* Top margin */
div.main,
p {
  margin-top: 10px;
}

If we discover that div.footer also needs a top margin, we add it to the selector instead of making a new rule. I bet someone else has come up with this style (and probably a better name for it), but I am unaware of it. I also guess that this was one of the original intentions of the CSS authors. In practice, in my experience, this is hard to keep up. Somehow, I do not have the discipline to keep the styles separated into their own rules. CSS properties that are related to div.main and blockquote, but not to dorothy slip into that first rule, and then all is lost. Maybe a professional could do better, but I have never met one.

I do not have that problem with LESS. It is simple to define a mixin once I identify a consistent set of properties. I can then reuse it wherever I want.

Cascading

LESS and Sass provide a pretty good, but partial, solution to the cascading problem. The cascading problem is basically one of complexity. There are too many places for the value of a particular property for a particular element to be set. And the rules for determining the precedence of all of those places are too complex.

The value of a CSS property is determined by these factors:

  • The order of CSS imports
  • The number of classes mentioned in the CSS rule
  • The order of rules in a particular file
  • Any inherits settings
  • The tag name of the element
  • The class of the element
  • The id of the element
  • All of the element's ancestors in the HTML tree
  • The default styles for the browser

It is just too many factors. Yes, the wisdom is to keep everything clean and simple. That works for small projects but at some point, cascading rules will bite you.

Jason Zimdars shows the solution to cascading they came up with at 37 Signals. He shares a good analysis of the problem and how LESS can alleviate some of the pain.

For the first time we could write CSS that we knew wouldn't cause problems later on because they couldn't cascade out of control. This opened us up to create elements with self contained styles that could be dropped onto most any page and they'd just work.

Sounds like the holy grail of separation of presentation from content!

By using nested LESS rules and child selectors, we can avoid much of the pain of cascading rules. Combining with everything else, we define our styles as mixins (and mixins of mixins) and tie the styles to the HTML with nested rules which mimic the structure of the HTML.

Perfect, final solution? Not quite.

Further

There are a few more issues to deal with. LESS and Sass were defined as supersets of CSS. That means that your valid CSS files are automatically LESS files as well, which means you can just start using the LESS compiler.

But it also means that LESS has inherited all of the problems it has no solution for. What I will suggest is that we need a subset of CSS to move further, and I will attempt to choose that subset. I would love to hear your suggestions, as well.

Yes, nested rules help you deal with cascading, but there are other issues with cascading. Mixins cannot really help you with the box model. No amount of variables and arithmetic can make two divs have the same width.

I will go through the problems one by one.

Cascading, again

Let me put it bluntly, cascading was a mistake on the part of the authors of CSS. It has a nice abstract purity to it, but it does not work well in practice.

With hindsight, we see that we really only want one level of cascading. The CSS reset was a beautiful invention which neutralized differences between browsers. The CSS reset cut off cascading from the default browser styles and gave you a fresh base to start with. That is really all of the cascading that you want: cascading to a sane default. Other than that, it turns into a mess of spaghetti.

Sometimes it seems that you want some cascading. For instance, you want to set the font family of the entire document. So you declare body { font-family: 'Comic Sans'; } and call your job done. In such a declaration, you are implicitly relying on the inheritance of the font-family property down through the document tree. In fact, if you want every element to have a certain font, you should just say it: * { font-family: 'Comic Sans'; } This has the same effect as a CSS reset: set the default styles for everything in one place.

This implies a rule: Reset once, then avoid cascading. We now just have to systematically apply it. Here is what our setup looks like now:

HTML, reset, mixins, and
style

No cascading means we must restrict ourselves to never select the same elements with different rules. I cannot say how we can do this strictly. But we can define some guidelines.

  1. Only bare (classless + non-nested) selectors may occur in the reset.
  2. No bare selectors may occur in the LESS rules.
  3. No selector may be repeated in the LESS rules.

These guidelines will limit the amount of cascading even further when combined with Zimdars' solution.

Common mistakes

I call them mistakes for lack of a better word, but really the blame lies on CSS.

Box model

The box model sucks. But we can avoid some of the easy errors to make.

One mistake is what happens when you define the left-margin but not the right-margin. In such a situation, where does the right-margin get determined? Cascading.

And what happens when I set the width to 100%? What if a padding is cascaded in? Oops.

How to deal with this? Do not use individual CSS properties where a compound property exists.

I propose to boycott the following properties:

  • left-margin, right-margin, top-margin, bottom-margin; use margin instead
  • left-border, right-border, top-border, bottom-border; use border instead
  • left-padding, right-padding, top-padding, bottom-padding; use padding instead
  • width, height; use the .dimension mixin instead

To get more sane behavior, we define this mixin:

.dimension(@w,@h) {
  width: @w;
  height: @h;

  margin: 0;
  padding: 0;
  border-width: 0;
}

This forces you to set width and height at once, and it resets the margin, padding, and border (which affect actual width, thanks to the box model). You can still override them, you just have to do it explicitly. This does not solve the entire problem of the box model, but it helps cut out a lot of surprising behavior.

My argument for using this mixin is that any time you are setting the dimensions of an element, you should also be explicit about the margin, padding, and border at that point, since they affect the box model.

Font color

Now I will pick some nits.

How many times have you seen this code?

body {
  color: black;
  a:link {
    color: blue;
  }
  a:hover {
    color: red;
  }
  a:active {
    color: blue;
  }
  a:visited {
    color: purple;
  }
}

Too much! And I always forget one of them. Time for a mixin.

.font-color(@f,@a:blue,@h:red,@c:blue,@v:purple) {
  color: @f;
  a:link {
    color: @a;
  }
  a:hover {
    color: @h;
  }
  a:active {
    color: @c;
  }
  a:visited {
    color: @v;
  }
}

Again, the pattern is clear: what you do not set explicitly gets reset to a default.

Conclusion

Separating style from content was never fully achieved with CSS. LESS (and Sass) finally allowed the separation to occur. And, using LESS, we can begin to round off the sharp edges of CSS. But instead of adopting a superset of CSS, we should be looking to subset CSS and replace problematic CSS properties with mixins. The subset could be enforced with a linter.

These recommendations are a good start, but there is still a long way to go.

Post script

There is one final reflection into CSS cascading that I wanted to mention but could not find a place for it above, mainly because it is not a problem so much as an inconvenience. I have often wondered why in CSS, element styles (styles defined in the style attribute of an HTML tag) take precedence over all other styles. Similarly, why do styles defined in the HTML (in a style tag) take precedence over those that are linked to externally? It has always made more sense to me that it should be the exact opposite. An HTML page could define default styles for its elements, which would be carried in the page, and overriden with an external stylesheet.

However, the actual rules dictate that I must edit the HTML file if I want to change the style of an element with an element style. In this not the exact opposite of the intention of CSS?

Sean Allen
Sean Allen
Your friendly reminder that if you aren't reading Eric's newsletter, you are missing out…
👍 ❤️
Nicolas Hery
Nicolas Hery
Lots of great content in the latest newsletter! Really glad I subscribed. Thanks, Eric, for your work.
👍 ❤️
Mathieu Gagnon
Mathieu Gagnon
Eric's newsletter is so simply great. Love it!
👍 ❤️