Maintainable CSS in Large Scale Web Projects

April 16, 2014

Blog | Technology | Maintainable CSS in Large Scale Web Projects
Maintainable CSS in Large Scale Web Projects

Maintainable CSS in Large Scale Web Projects

Writing CSS is easy. All you need to do is open a text editor and start typing. Even if you make mistakes, browsers will still do their best to apply as many styles as they can. As it is, it only takes 247 bytes to make HTML responsive:

/*
    FLUIDITY v0.1.0 @mrmrs - http://mrmrs.cc MIT
*/

/* Responsive Utilities */
img, canvas, iframe, video, svg {
    max-width: 100%;
}

/* Wrap tables or pre elements in a div with this class */
.overflow-container {
    overflow-y: scroll;
}

/* Aspect ratios for media objects i.e canvas, iframe, video, svg etc. Defaults to 16x9 */
.aspect-ratio {
    height: 0;
    padding-top: 56.25%;
    position: relative;
}

.aspect-ratio--object {
    bottom: 0;
    height: 100%;
    left: 0;
    position: absolute;
    right: 0;
    top: 0;
    width: 100%;
    z-index: 100;
}

The difficulty with CSS is maintaining it. You might have everything working perfectly in IE11 today, but the next thing you know IE12 comes out and everything is broken again. Browser bugs are a fact of life for front-end developers, so it’s always a good idea to make sure that it’s easy to quickly get in and fix any issues that crop up.

For small-scale projects, a single .css file tends to be enough.

Once that file grows to more than a couple hundred lines, organizing code and avoiding duplicate code becomes a real challenge.

CSS Pre-processors to the rescue

Thankfully with CSS preprocessors like LESS and Sass, monolithic CSS files can be broken down into smaller, more manageable ones. Variables can be used so that colors and sizes can be changed in bulk. Mixins can be used to apply common styles across disparate selectors.

Even with a preprocessor, CSS can balloon until it’s a Big Ball of Mud. Fortunately, with a bit of planning and structure, CSS can be broken down into simple modules that are easy to manage.

Ground rules

Note: For this article I’m going to be focusing on LESS, however the same rules generally apply to Sass as well.

It’s important to keep code clean and readable, so a style guide is a must. Different teams have different needs, so be sure to pick the rules that work for you. The exact rules you pick don’t matter, what matters is that they’re standardized and that everyone working on the project is aware of the rules. Keeping a README file describing the rules in with the source files is helpful. The rules should be seen as a living document, subject to change over time to best meet the current needs of the team.

As a project matures, the rules governing how code is written should mature as well.

The high-level goal is to keep the code readable. If the code is difficult to read, then it’s likely that there are bugs hiding in it. Refactor it so that it can be easily understood at a glance. I recommend the following rules to help achieve this goal.

Use consistent indentation

We default to 4 spaces with braces after the selector, but the number and brace location don’t matter as long as everyone on the team is using the same thing. Inconsistent indentation in LESS can make it easy to overlook problems with nested styles.

One selector per line

When writing compound selectors, be sure that each selector gets its own line. This rule makes it easier to spot errors. For example the following code could be ambiguous:

//Bad Example
.foo, .bar .baz {...}

Was the intent to select .foo and .bar .baz, or .foo, .bar, and .baz? Rewriting would have avoided this confusion.

Here it’s easy to see that a comma is missing, which is easy to correct:

//Good Example with error
.foo,
.bar
.baz {...}

Alternatively, this next example shows that selectors of different specificity are being used intentionally:

//Good example
.foo,
.bar .baz {...}

One rule per line

I’ve worked on projects where all the rules were listed horizontally after the selector. Making modifications was very tedious. When all the rules are laid out horizontally, you end up having to read the entire line before you can find where you need to make a change.

Style duplication was a serious issue as well:

//can you see the problem here?
.example { display: block; color: red; margin: 12px 0 5px; border: 1px dashed green; padding: 5px 11px; color: blue; }

The color rule was listed twice, but it’s not exactly easy to spot.

//how about now?
.example {
    display: block;
    color: red;
    margin: 12px 0 5px;
    border: 1px dashed green;
    padding: 5px 11px;
    color: blue;
}

Lists are much easier to process because they can be scanned vertically and then read horizontally when the proper item has been found.

Use consistent rule order

The previous example with the duplicate color rule would have been avoided by using a consistent rule order as well.

My preference is to use alphabetical order. It’s the fastest to explain to other developers, and is much easier to remember. Mixins should be placed in the beginning of the code block so that the subsequent rules override the mixin. Additionally, -prefix- rules should precede the standardized ones when they are being used. Nested selectors should be listed last:

.example {
    .mixin();

    color: red;
    -moz-border-radius: 3px;
    -webkit-border-radius: 3px;
    border-radius: 3px;
    display: block;

    .foo {
        ...
    }
}

Lowest specificity selector possible

When writing new selectors in CSS, it’s important to pick a selector with low specificity. Specificity is the measure used to determine which style rules will take effect, and which ones will be overridden. When two selectors match the same element, the selector with the higher specificity will override the lower specificity selector. In cases of a tie, the later selector wins.

It’s common to see developers write selectors with a high specificity thinking that it’ll prevent the styles from applying to elements that they didn’t intend. Unfortunately it also means that the styles aren’t then applied to new elements when they’re used in new locations. If you know you want widgets to be red in the header, but you’re not sure about the footer, style all widgets red, and use themes to override the widget styles when they’re used in the footer:

//bad example
.header .widget {
    background-color: red;
}
//good example
.widget {
    background-color: red;
}

The reason you should prefer lower specificity selectors is to avoid specificity battles. If a selector such as #foo .bar baz is used, overriding it will require an ID, a class, and an element selector at a minimum. Long selectors easily get out of hand and are difficult to track down where they actual affect the page.

Avoid nesting aka “Arrow Code”

While preprocessors allow for nested styles, they should only be used when the nesting is actually desired in the CSS output. Avoiding nesting helps to reduce the specificity of selectors.

//bad example
html {
    height: 100%;

    body {
        height: 100%;
        margin: 0;
        overflow-y: scroll;
        padding: 0;

        ul {
            padding-left: 20px;

            li {
                margin: 10px 0;
            }
        }
    }
}
//good example
html {
    height: 100%;
}

body {
    height: 100%;
    margin: 0;
    overflow-y: scroll;
    padding: 0;
}

ul {
    padding-left: 20px;
}

li {
    margin: 10px 0;
}

Prefer classes and attributes over IDs

IDs must be unique on a page, so at most an ID selector will affect a single element, or children of a single element. The higher specificity means that any subsequent overriding will also need to contain an ID selector. If nothing else, the attribute selector ([id="..."]) should be used in place of the ID selector (#...).

Prefer elements over classes and attributes

I’ve seen many projects where classes like .heading, and .subheading are used when h1 and h2 would have been more appropriate. It’s easier to create new content when you can use plain HTML elements like <h1> rather than having to remember to add extra classes (<h1 class="primary-heading heading large text foo bar baz">).

Bringing order to the files

Once the ground rules have been set, it’s important to organize the source files. It doesn’t matter how beautiful and consistent the code is, if it’s all in a single monolithic stylesheet it’s still going to be a mess to maintain, especially when media queries are interspersed. With preprocessors allowing @import statements, the Single Responsibility Principle can be applied to .less files. Each .less file should be responsible for a specific feature set.

I tend to use an assets folder to store all static project assets and keep them distinctly separate from server-side code. Under the assets folder, subfolders can be used for each file type:

/assets/
       /css
       /images
       /js
       /less
       ...

.less files in the /assets/less folder can then be compiled into .css files in the /assets/css folder.

Main .less file

The main .less file should be used to contain only @import statements. The main file will be built into the actual .css file output.

In this example, /assets/less/styles.less compiles into /assets/css/styles.css:

@import 'core';

@import 'widgets/foo';
@import 'widgets/bar';

@import 'content/accordion';
@import 'content/carousel';
@import 'content/videos';

@import 'variables';

core.less file

I use a core.less file to contain the vast majority of element-specific styles. The styles in core are meant to set the standard used across the entire project. If the default color of links should be orange, then core.less would contain:

a {
    color: orange;
}

A good starting point is to use normalize.css, which helps to start with a common base point cross-browser.

Subfolders

Below the /assets/less folder are subfolders to contain files that relate to a specific feature. The /assets/less/modules folder would contain files that relate to a specific module; the /assets/less/widgets folder would contain files that relate to a specific widget; the /assets/less/forms folder would contain files that relate to a specific form. It’s useful to have the folder structure mirror that of the folder structure used for sever controls as well. That way when a server control exists at /widgets/foo.ascx, the .less file that drives its styles can be found at /assets/less/widgets/foo.less. This parity helps to ensure that when a new control is created, the styles for the control have a defined place. More importantly, when a control is removed, the styles for the control can be removed as well.

Variables

As variables in LESS are either global or scoped to a specific selector/mixin, it’s a good idea to use a single file or collection of files to store any and all global variables. I use variables.less, although it could easily be broken into multiple files such as /assets/less/variables/colors.less and /assets/less/variables/indexes.less.

Color variables

All color values should be stored in variables. It helps to maintain thematic consistency, and it’s much easier to maintain. Sometimes clients ask to change a color across the board. If they’re set in variables, it can be modified in one place to change it everywhere.

Z-index Variables

Like colors, all z-index values belong in variables as well. Unlike colors, every z-index rule should use a unique variable. The reason for this is so that elements that have their z-index set can be rearranged without needing to search through disparate files in search of the other z-index rules. It’s much easier to see how components of the page will stack when the variables are listed in one place. The variables should be ordered by their values.

This sort of organization also helps to avoid the obscene z-index values of 999999 that are often thrown around code when a developer has given up trying to find the z-index of the element that should be below it.

For consistency I also make sure that the z-index variables are suffixed with -index:

@header-index: 0;
@content-index: 0;
@footer-index: 0;
@overlay-background-index: 1;
@overlay-index: 2;

Summary

There’s a lot of this that I’ve come up with over the years from experimentation and seeing things go wrong. It’s certainly not perfect, but if you follow these guidelines the violent psychopath who has to maintain your code will be less likely to come knocking on your door after running git blame.

Tim Leverett

Senior Developer
Tags
  • CSS

Recent Work

Check out what else we've been working on