Separation, Abstraction, and Cascading in CSS

December 29, 2012

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:

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:

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?

For more inspiration, history, interviews, and trends of interest to Clojure programmers, get the free Clojure Gazette.

Learn More

Clojure pulls in ideas from many different languages and paradigms, and also from the broader world, including music and philosophy. The Clojure Gazette shares that vision and weaves a rich tapestry of ideas from the daily flow of library releases to the deep historical roots of computer science.

You might also like

The Case Against Curly Quotes

November 05, 2012

A lot of web software converts ASCII single- and double-quote characters into special, non-ASCII open and close versions. These are sometimes referred to as "smart quotes". Their less intelligent ASCII equivalents are "dumb quotes". Smart quotes undoubtedly look better.

Here is an example.

"Some text" becomes “Some text”.

Very pretty, no? There is no smart quote key on your keyboard, so in order to make them, your software will convert them automagically. I say "automagically" because they are often converted incorrectly. There are no simple rules to convert them correctly.

I used to upgrade my dumb quotes to smart, but a couple of years ago I decided against it. Although I would like to have prettier quotes, they annoy me on other people's sites, even when they are correctly chosen.

Specifically, they break copy-paste. When you copy text with smart quotes (and smart apostrophes, smart dashes, and other smart characters), you get crap in your text. I like copy-paste and I do not want it broken on my site. In fact, I would love it if the world stopped using smart quotes.

Google Common Lisp Style Guide

October 11, 2012

In April 2011, Google acquired ITA Software. ITA provides airline flight search for several of the major companies. The interesting thing is that ITA's search engine was written in Common Lisp.

This style guide shows that Google is adopting the existing Lisp code base instead of rewriting it. Not so surprising since the code is, from what I have heard, well-engineered. But a bit surprising since Google chose Python over Lisp some time ago. Peter Norvig explains the choice a bit on Reddit and Hacker News.

In addition, it also shows that Google is affecting ITA with its culture, to some degree. Google uses this format for all of its style guides.

Anyway, I love style guides!

Clojure/ClojureScript: One Language to Rule the Web

October 11, 2012

Nice intro to Clojurescript.

Static vs. Dynamic Typing

October 05, 2012

I was reading this question on Stack Exchange asking what is possible in a dynamically typed language that is impossible in a statically typed language. Most of the responses were terrible. It reminded me that the level of discussion surrounding the topic of types in general is very disappointing. Basically, most people do not know what they are talking about.

Luckily, one of the answerers posted a link to this post about debating type systems. I had never read it before and I am glad I have, now.

The author takes the approach of describing how people generally use typing terms as opposed to prescribing a definition. His main point is that the terms are generally misunderstood so much that trying to discuss them is not productive. There is no agreed-upon definition in most contexts. I agree. I wish it were not the case, though.

But there was also another gem in the article.

Dynamic and static type systems are two completely different things, whose goals happen to partially overlap.

I think this point is right on. Static type checking is only one possible form of static analysis. And the goal of static analysis is to detect errors as soon as possible. Dynamic typing is used more to perform dynamic type dispatch (that is, runtime polymorphism). Because they have such different goals, why are we even comparing them?

I think a lot of it is C++'s fault. C++ took the idea of Classes and inheritance from OOP and conflated them into static types. But I'm not an expert. Maybe someone did that before C++. By the way, this is probably 90% of why people hate static typing. When static typing prevents something useful (like duck typing) but does not present an alternative to that use, you wish you did not have static typing.

The real point is that static typing and dynamic typing are not inherently mutually exclusive. For instance, Java has static and dynamic typing. That is, you can ask for the type of an object at runtime, and the types of variables are checked at compile time. Clojure has some form of static analysis (no static type analysis) and inherits the runtime types from Java.

The question "which is a subset of the other?" is actually meaningless. But the question "if you had to choose a language that only had one, then add the other to it, which would you pick?" is becoming increasingly relevant. Haskell recently added what some consider to be dynamic typing behavior (throwing type errors at runtime instead of at compile time). And people are working on static type systems for Clojure.

I do not think that this is a subjective question. I think it has one possible answer in the general case. And I would like to present the argument to open up the discussion. I will use Haskell (with the most advanced static type system in the world) and Clojure (it is comparable to Haskell and dynamically typed).

In Clojure, type information is purely informational. It does have Java's inheritance hierarchy, but that is a mere legacy to interoperate with Java. The important part of Clojure is that every value has a type and the program can know the type at runtime. This could be considered the minimal possible dynamic type system: present types at runtime and defer to the program to do what it wishes with them. The semantics of evaluation are independent of the type system.

In Haskell, type information exists only at compile time. That is, the compiler figures out the type of everything, decides whether to allow the program, then compiles it (if it is allowed). Types are complected with semantics. Types determine what code is run. Further, because the type checker has to be able to infer all types, the language has been limited to the subset of possible programs that are checkable. The semantics of the language are limited by the type system.

So what we have is a semantics that is independent of types (Clojure) which we can augment with different static type analyses and potentially catch errors, or we have a language that has already compromised its semantics to the type system and add runtime type annotations, which are effectively useless.

Just one last word: I am not a dynamic typing zealot. I use Haskell more than Clojure. And I am very excited to see static analysis come to Clojure. However, the static typing crowd seems to have the rhetorical upper hand at the moment. I just want to have better discussions.

The Impedance Mismatch is Our Fault

October 05, 2012

Stuart Halloway continues the world-round campaign of iconoclastic and well-argumented presentations that spring from Clojure's programming model and now Datomic's architecture. Stuart gives hard-hitting, reasoned practices that make our software better in measurable ways.

This is just one of the many ways Clojure is having (and will have) a huge impact on programming in any language. Immutable data by default is obvious. A model of time, while not so obvious, will seem like manual memory management does now. Sure, you can work with it, but who would give up garbage collection? Similarly, having a better model of state change (transactional or otherwise) enforced by the language will be something we will not want to live without.

Separating data from code will take years, maybe a generation, to catch on. Our field has drilled the Java model of Object-Orientation into students for years. But the change has already started. Though not being led by Clojure, JSON is now a universal data format accepted by nearly every language, and the fact that the standard does not allow code in JSON is significant.

But this talk goes a little further and proposes that we think of databases differently. Stuart does a good job of breaking down why OO and RDBMS don't fit well together, and proposes a solution that leaves them both in the dust.

Enjoy.

David Nolen on Clojure's core.logic

September 28, 2012

A bootleg video of a talk from Strange Loop 2012.

I do not think the official videos are up yet. For those of us who could not make it to the conference, this is great, even though it was recorded with an iPhone. You can hear Nolen well and see the slides.

The presentation starts with a short review of how to use core.logic before Nolen goes into the implementation details. I still need to spend some time understanding the code myself. core.logic is based on miniKanren, described in The Reasoned Schemer.

Actually, I have just bought a copy of The Little Schemer, the first book in the series of which The Reasoned Schemer is a part. I am trying to catch up on all of these Lisp classics. I will write a review when I am finished, but so far so good.

ClojureScript Anatomy

September 27, 2012

Michael Fogus:

What is the greatest limitation of Haskell's type system? The greatest limitation in Haskell's type system is it only does Haskell's type system.

I totally agree. Almost two years of working in Haskell has convinced me of the benefit of static type analysis. It can catch a lot of bugs. But Haskell's type system also increases the amount of code and the surface area of your interface. The types become a point of coupling. They become hard to change without changing everything that uses them.

This rigidity is where their power comes from, and also their annoyance. Well-chosen types can stabilize a system in a good way. Mistakes in the types, or changes in their requirements, stabilize the system in a bad way.

I would love to see a type checker that could handle non-discriminated union types (Haskell has discriminated unions, which force you to box and unbox values, adding to code size and coupling to the types). It would be nice to have the type checker say "Function foo takes either a String or a Number, but on line 34 you pass it a String or Number or HashMap."

Another type of analysis I would like to see is whether a function is pure, does IO, or accesses mutable state. You can tag all basic IO operations as IO. Then anything that calls one of those operations also gets the tag IO. Similarly for mutable state. Haskell makes IO a type so that it can be statically checked, because Haskell only does type checking. It is not really a type, though, and does not need static type analysis.

Another great advantage of static checking in a dynamic language is that the definition of functions can change at any time. So function foo could compile, but with a warning that it calls function bar incorrectly. When you correct function bar, the warning goes away automatically.

Those are some ideas I had watching that video and thinking about static analysis in Clojure. I am quite excited.

Douglas Crockford on Javascript

September 21, 2012

These are amazing. I learned so much about the browser and Javascript that I want to weep for the web.

On the configurations of data and functions

September 21, 2012

Rich Hickey recently commented (I cannot remember where) that there are a limited number of useful configurations of data. He claimed that the Clojure literal syntax is just about right. The one thing to add to it is the extensibility that we now have with 1.4.1

Part of the context was that we as programmers should be separating data from process. The separation makes our data more reusable. A string is a string in any language. An array is an array. Data is data and should be regarded as such. This allows for much more reuse than object oriented programming ever has. We can see the tremendous success of JSON as a case in point: a well-defined, constrained data language that gets it mostly right.

But Hickey's point goes a little farther, I think. It is not just data that is reusable, but functions as well. Look at the enormous savings in the amount of code required to get a Swing app running when you use Seesaw. I am not just talking only about the incessant boilerplate anonymous class declarations Swing forces you to write. I am talking also about the fact that I can use any function to handle an event. I do not need to figure out what class the thing is expecting. Defining a subclass of a single class is inherently non-reusable.

And it is not just that I don't need to know the class. I can use a function written in a library that has no dependencies on Swing or Seesaw at all. A function is just a function. It is universal and reusable, not because of some well-modeled domain classes, but because it is abstract and applies to no domain in particular.

While Hickey is working on a universal, extensible data format, who is working on the universal function format?


  1. See also EDN