Monday, August 27, 2012

CSS Good Practices

Most web developers have realized that Javascript is a first-class citizen in web development, but the same is not true of CSS. CSS is still "the wild west" and I believe that this is because CSS falls between the chairs of developers and designers. CSS is also a first-class citizen and it needs as much love as Javascript to avoid becoming a maintenance nightmare. Here are some ideas that I believe are good practices to follow.

Use a pre-processing language

CSS does not allow us to use standard abstractions such as variables and functions. This makes duplication common practice, but there is a simple solution to this problem. Use a pre-processing language! There are a number of good alternatives, Sass, Less, Stylus, etc. My preference is the SCSS dialect of Sass but, I have no problem using any other variants as long as they give me the abstractions I need.

The first abstraction is variables, which are really constants since they are commonly never changed. Consider the following example:

.info-message {
    color: #eee;
    background: #1e1;
    margin: 5px;
    padding: 5px;
}

.error-message {
    color: #eee;
    background: #e11;
    margin: 4px;
    padding: 5px;
}

What do these values mean? Is the color #eee, referring to the same concept in both info-message and error-message? Should the margin and padding be the same in both cases, or should it be off by one pixel? Looking at this CSS, it is not possible to tell. Contrast it with the following SCSS.

$info-message-foreground-color: #eee;
$info-message-background-color: #1e1;

$error-message-foreground-color: #eee;
$error-message-background-color: #e11;

$message-margin: 4px;
$message-padding: 5px;

.info-message {
    color: $info-message-foreground-color;
    background: $info-message-background-color;
    margin: $message-margin;
    padding: $message-padding;
}

.error-message {
    color: $error-message-foreground-color;
    background: $error-message-background-color;
    margin: $message-margin;
    padding: $message-padding;
}

Isn't that lovely! Now there are not doubts about what anything means anymore. It turned out that #eee was referring to different concepts in info-message and error-message

Variables is enough reason to use a pre-processor, but since we are now already using a pre-processor we may as well take advantage of another killer feature, mixins.

Mixins allows us to chunk up common concepts into reusable blocks. Continuing with the example above we can introduce a mixin, message-layout to get rid of the duplication of margin and padding.

...
$message-margin: 4px;
$message-padding: 5px;

@mixin message-layout {
    margin: $message-margin;
    padding: $message-padding;
}

.info-message {
    color: $info-message-foreground-color;
    background: $info-message-background-color;
    @include message-layout;
}

.error-message {
    color: $error-message-foreground-color;
    background: $error-message-background-color;
    @include message-layout;
}

Another way to do this is to create a parameterized message-box-mixin.

$info-message-foreground-color: #eee;
$info-message-background-color: #1e1;

$error-message-foreground-color: #eee;
$error-message-background-color: #e11;

$message-margin: 4px;
$message-padding: 5px;

@mixin message-box($message-foreground-color, $message-background-color) {
    color: $message-foreground-color;
    background: $message-background-color;
    margin: $message-margin;
    padding: $message-padding;
}

.info-message {
    @include message-box($info-message-foreground-color, $info-message-background-color);
}

.error-message {
    @include message-box($error-message-foreground-color, $error-message-background-color);
}

Sweet! As you can see from this simple example, a pre-processor is not really something that we should do without. I consider it professional malpractice not to use one.

This is only the tip of the iceberg. Now that we are using a pre-processor we can make use of pre-packaged mixin libraries such as, Bourbon or Compass, relieving us from having to write all the vendor-variants all over again. But don't go overboard, use with moderation.

Everything with moderation, including moderation. --Oscar Wilde

Pre-processor usage

When using a pre-processor there are a few ways to use it.

  1. Use a framework that supports pre-compilation, such as Rails or Express
  2. Use a watcher and check in the generated CSS.
  3. Use a watcher, but don't check in the generated CSS. Deploy using a build-script that does the pre-compilation.

My preferred choice is to use a framework that supports it, but if that is not an option, I prefer to not check in the generated CSS. There is a small problem with this approach when using Git as a deployment tool, like Heroku does. This problem is best solved with some clever scripting and an extra deployment branch that is used to hold the generated CSS for deployment.

Watchers come in many forms, sass --watch, Watchr, Guard, and Livereload to name a few.

Variable and Mixing Naming

Variables names should reflect the names of the domain. The domain, in this case, is the name of the designers, commonly called the legend. They know what it means when they talk about a color palette, primary-color, etc.. We should know what this means too, and we should use these names.

Mixins comes in two forms, domain-mixins related to our application, and utility-mixins overcoming deficiencies in browsers etc.

Domain-mixins are similar to variables and should be named similarly. Utility-mixins are more general, they can often be reused across projects and should be named appropriately and separated out into their own file.

CSS Naming

Naming is another part of CSS that is inconsistent to say the least. The most common way of naming CSS entities is to use dasherized names, such as my-lovely-tapir and left-margin. I believe this is the best way for a couple of reasons.

  1. It is the way that CSS internal properties are named, left-margin, border-radius, -webkit-search-decoration.
  2. It makes a clear contrast between CSS and Javascript properties.

Sometimes it may be convenient to use CamelCase or snake_case style to interact with another programming language. Avoid falling for this temptation. It is not difficult to write a camelcase, snake\_case or, dasherize function to convert between the different styles.

IDs and Classes

Since IDs are global in the page, they should be named with specific names that clearly identifies what they are on the page. Classes, on the other hand, are not unique, and should be named with more general and succinct names to allow for reuse across scopes.


/* Specific singleton elements are named with specific names. */
#sidebar {}
#send-email-button {}
#all-animals
#animals-in-tapir-cage

.selected {
    /* Common for all selected elements on page. */
}

#all-animals .selected {
    /* Specific CSS for selected elements in #all-animals. */
}
#animals-in-tapir-cage .selected {
    /* Specific CSS for selected elements in #animals-in-tapir-cage. */
}

Sass Nesting can be put to good use when writing this kind of CSS, but beware of creating overly long selector chains, since this will slow down the CSS parser as well as making the code less clear. There is never a need for two IDs in a selector, since IDs are unique. (Unless you are overriding someone else's crappy CSS, that has put this horrendous method to "good" use :)

It is also possible to avoid using the generic .selected element altogether by making it a mixin instead of class, but I believe that the method above still has a place.

Structure and Semantics

The HTML provides the structure of our GUI but, it also contains the semantics of our GUI. When we put a class or an ID on an element, we are naming that part of the DOM-tree. We should name it in a way that makes is clear what the meaning of this part of the tree is. It is not different from naming a class or an object in a real programming language, and just as important. Name it so that it easy to reason about that part of the tree. What is this area used for? What does it contain?

If an HTML element makes it clear, it is better to use the element than to use a class. It is better to use <article> than <div class='article'>. On the other hand it is better to use <div id='sidebar'> than <aside> since aside has a different meaning, than commonly believed.

aside, a remark that is not directly related to the main topic of discussion -- dictionary.com

There is absolutely no reason to use "style-classes".

/* Horrible use of CSS */
.mb5 {
    margin-bottom: 5px;
}
<!-- Horrible use of CSS -->
<div class='mb5'>
...
</div>

Not only does this suck from a semantic and structural perspective it is also awful because it is less clear than style='margin-bottom: 5px' and it encodes the value in the name, a few weeks later the code will be littered with mb5s and the CSS will change to 8px, making insanity the norm.

/* Horrible use of CSS */
.mb5 {
    margin-bottom: 8px;
}

But there are also other uses of style-classes, that group a set of properties together in order to reuse them on many different elements. If you are not using a pre-processor I understand the need for this but, when you are using a pre-processor this behavior can be replaced by mixins.

Grids

Grids are another type of style-classes that I believe that we can remove from our HTML. I am not experience enough in responsive design to confidently say that it is possible to do it well without polluting the HTML with span4, offset3, etc. but, I feel that this type of styling can be moved into pre-processor functionality like the Bourbon#flex-grid

Summary

CSS is a first-class citizen of web development, but it is not good enough to be used without a pre-processor providing us with better abstractions. A good pre-processor allows us to clean up not only our CSS but our HTML too. That is a very good payoff for the small price of a small compilation step.

2 comments:

Anonymous said...

Great post! I think CSS should be the perfect intersection where developers and designers agree on sane practices and can really communicate about what they're trying to achieve. Developers can provide the raw structure, and designers can add their magic. It's a huge waste not to treat it with the same care as the rest of the code base.

And you managed to squeeze in my favorite Oscar Wilde quotation! Can't do better than that.

Anders Janmyr said...

@Felix, Thanks. I agree with the Oscar Wilde quote, you can't beat recursion!