Chapter 8. The Ten Commandments of Sane Style Sheets

  1. Thou shalt have a single source of truth for all key selectors
  2. Thou shalt not nest (unless thou art nesting media queries, overrides or thou really hast to)
  3. Thou shalt not use ID selectors (even if thou thinkest thou hast to)
  4. Thou shalt not write vendor prefixes in the authoring style sheets
  5. Thou shalt use variables for sizing, colours and z-index
  6. Thou shalt always write rules mobile first (avoid max-width)
  7. Use mixins sparingly (and avoid @extend)
  8. Thou shalt comment all magic numbers and browser hacks
  9. Thou shalt not inline images
  10. Thou shalt not write complicated CSS when simple CSS will work just as well

Blessed are those that follow these rules for they shall inherit sane style sheets.

Amen.

Why the ten commandments?

The following, highly opinionated, set of rules came about as a way to author predictable style sheets across teams of developers. The rules all address a different need and for the most part can be enforced with tooling. When there is just one CSS developer on a project, the tooling may seem superfluous. However, beyond a couple of active developers the tooling will earn its time investment time and again. We will deal with the tooling to 'police' the rules in the next chapter. For now, let's consider the syntax and the rules themselves.

Tooling

To achieve more maintainable style sheets we can lean upon PostCSS, a piece of CSS tooling that allows the manipulation of CSS with JavaScript. The curious can look here for more information: https://github.com/postcss/postcss

PostCSS facilitates the use of an extended CSS syntax. For the purpose of authoring, the syntax used borrows heavily from Sass. This provides functionality to make our authoring style sheets easier to maintain.

Using PostCSS we are able to make use of:

Practically, PostCSS is similar in functionality to a CSS pre-processor such as Sass, LESS or Stylus.

Where it differs is in its modularity and extensibility. Rather than 'swallow the whole pill' as is needed with the aforementioned pre-processors, using PostCSS allows us to be more selective about the feature set we employ. It also allows us to easily extend our feature set at will, either with any number of 'off the peg' plugins, or, by writing our own plugins with JavaScript.

In addition it allows us to perform static analysis of the authoring styles via linting and can fail builds when undesirable code is authored.

Rationale

When we are authoring ECSS, we want to avoid producing CSS that suffers from being overly specific, littered with unneeded prefixes, poorly commented and full of 'magic' numbers.

The following 10 rules set-out what are considered to be the most important rules to achieve this goal.

Definitions used throughout:

Let's now consider each rule and the problem it aims to solve.

1. Thou shalt have a single source of truth for all key selectors

In the authoring style sheets, a key selector should only be written once.

This allows us to search for a key-selector in the code base and find a 'single source of truth' for our selector. Thanks to the use of an extended CSS syntax, EVERYTHING that happens to that key selector can be encapsulated in a single rule block.

Overrides to the key selector are handled by nesting and referencing the key selector with the 'parent' selector. More of which shortly.

Consider this example:

.key-Selector {
    width: 100%;
    @media (min-width: $M) {
        width: 50%;
    }
    .an-Override_Selector & {
        color: $color-grey-33;
    }
}

That would yield the following in the CSS:

.key-Selector {
  width: 100%;
}

@media (min-width: 768px) {
  .key-Selector {
    width: 50%;
  }
}

.an-Override_Selector .key-Selector {
  color: #333;
}

In the authoring style sheets, the key selector (.key-Selector) is never repeated at a root level. Therefore, from a maintenance point of view, we only have to search for .key-Selector in the code base and we will find everything that could happen to that key selector described in a single location; a single source of truth.

In all these instances the eventualities for that key selector are nested within that single rule block. This means that any possible specificity issues are entirely isolated within a single set of curly braces.

Let's look at overrides in further detail next.

Dealing with overrides

In the prior example, there was a demonstration of how to deal with an override to a key selector. We nest the overriding selector inside the rule block of the key selector and reference the parent with the & symbol. The & symbol, as in the Sass language, is a parent selector. It might help you to think of it as being roughly equivalent to this in JavaScript.

Standard override

Consider this example:

.ip-Carousel {
    font-size: $text13;
    /* The override is here for when this key-selector sits within a ip-HomeCallouts element */
    .ip-HomeCallouts & {
        font-size: $text15;
    }
}

This would yield the following CSS:

.ip-Carousel {
  font-size: 13px;
}

.ip-HomeCallouts .ip-Carousel {
  font-size: 15px;
}

This results in a font-size increase for the ip-Carousel when it is inside an element with a class of ip-HomeCallouts.

override with additional class on same element

Let's consider another example, what if we need to provide an override when this element gets an additional class? We should do that like this:

.ip-Carousel {
    background-color: $color-green;
    &.ip-ClassificationHeader {
        background-color: $color-grey-a7;
    }
}

That would yield this CSS:

.ip-Carousel {
  background-color: #14805e;
}

.ip-Carousel.ip-ClassificationHeader {
  background-color: #a7a7a7;
}

Again, the override is contained within the rule block for the key selector.

override when inside another class and also has an additional class

Finally let's consider how to do contain the eventuality where we need to provide an override where the key selector is inside another element and also has an additional class present:

.ip-Carousel {
    background-color: $color-green;
    .home-Container &.ip-ClassificationHeader {
        background-color: $color-grey-a7;
    }
}

That would yield the following CSS:

.ip-Carousel {
  background-color: #14805e;
}

.home-Container .ip-Carousel.ip-ClassificationHeader {
  background-color: #a7a7a7;
}

We have used the parent selector here to reference our key selector between an override above (.home-Container) and alongside another class (.ip-ClassificationHeader).

override with media queries

Finally, let's consider overrides with media queries. Consider this example:

.key-Selector {
    width: 100%;
    @media (min-width: $M) {
        width: 50%;
    }
}

That would yield this CSS:

.key-Selector {
  width: 100%;
}

@media (min-width: 768px) {
  .key-Selector {
    width: 50%;
  }
}

Again, all eventualities contained within the same rule. Note the use of a variable for the media query width. We will come on to that shortly.

Any and all media queries should be contained in the same manner. Here's a more complex example:

.key-Selector {
    width: 100%;
    @media (min-width: $M) and (max-width: $XM) and (orientation: portrait) {
        width: 50%;
    }
    @media (min-width: $L) {
        width: 75%;
    }
}

That would yield this CSS:

.key-Selector {
  width: 100%;
}

@media (min-width: 768px) and (max-width: 950px) and (orientation: portrait) {
  .key-Selector {
    width: 50%;
  }
}

@media (min-width: 1200px) {
  .key-Selector {
    width: 75%;
  }
}

With all the nesting of overrides we have just looked at, you may think it makes sense to nest child elements too? You are wrong. Very wrong. This would be a very very bad thing to do. We'll look at why next.

2. Thou shalt not nest (unless thou art nesting media queries, overrides or thou really hast to)

The key selector in CSS is the rightmost selector in any rule. It is the selector upon which the enclosed property/values are applied.

We want our CSS rules to be as 'flat' as possible. We DO NOT want other selectors before a key selector (or any DOM element) unless we absolutely need them to override the default key selector styles. Adding additional selectors and using element types (for example h1.yes-This_Selector):

For example, suppose we have a CSS rule like this:

In that above example, yes-This_Selector is the key selector. If those property/values should be added to the key selector in all eventualities, we should make a simpler rule.

To simplify that prior example, if all we want to target is the key-selector we would want a rule like this:

.yes-This_Selector {
    width: 100%;
}

Don't nest children within a rule

Suppose we have a situation where we have a video play button inside a wrapping element. Consider this markup:

<div class="med-Video">
    <div class="med-Video_Play">Play</div>
</div>

Let's set some basic styling for the wrapper:

.med-Video {
    position: absolute;
    background-color: $color-black;
}

Now we want to position the play element within that wrapping element. You might be tempted to do this:

That would yield this CSS (vendor prefixes removed for brevity):

Do you see the problem here? We have introduced additional specificity for our .med-Video_Play element when it is completely unneeded.

This is a very subtle illustration. However, it is important to be aware of, and avoid, doing this as we don't want rules like this:

Instead, remember that each key selector gets its own rule block. Overrides are nested, child elements are not. Here is that example rewritten correctly:

.med-Video {
    position: absolute;
    background-color: $color-black;
}

/* Center the play button */
.med-Video_Play {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

That would yield this CSS:

.med-Video {
  position: absolute;
  background-color: #000;
}

/* Center the play button */
.med-Video_Play {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

Each key selector is only as specific as it needs to be and no more.

3. Thou shalt not use ID selectors (even if thou thinkest thou hast to)

The limitations of IDs in a complex UI are well documented. In summary, they are far more specific than a class selector - therefore making overrides more difficult. Plus they can only be used once in the page anyway so their efficacy is limited.

With ECSS we do not use ID selectors in the CSS. They present no advantages over class based selectors and introduce unwanted problems.

In the almost unbelievable situation where you HAVE to use an ID to select an element, use it within an attribute selector instead to keep specificity lower:

[id="Thing"] {
    /* Property/Values Here */
}

4. Thou shalt not write vendor prefixes in the authoring style sheets

Thanks to PostCSS, we now have tooling that means it is unnecessary to write vendor prefixes for any W3C specified property/values in the authoring style sheets. The prefixes are handled auto-magically by the Autoprefixer tool that can be configured to provide vendor prefixes for the required level of platforms/browser support.

For example, don't do this:

Instead you should just write this:

.ip-Header_Count {
    position: absolute;
    right: $size-full;
    top: 50%;
    transform: translateY(-50%);
}

Not only does this make the authoring style sheets easier to read and work with, it also means that when we want to change our level of support we can make a single change to the build tool and the vendor prefixes that get added will update automatically.

The only exception to this scenario is non-W3C property/values that might still be desirable. For example, for touch inertia scrolling panels in WebKit devices, it will still be necessary to add certain vendor prefixed properties in the authoring styles as they are non-W3C. For example:

.ui-ScrollPanel {
    -webkit-overflow-scrolling: touch;
}

Or in the case of removing the scrollbar for WebKit:

.ui-Component {
    &::-webkit-scrollbar {
      -webkit-appearance: none;
    }
}

5. Thou shalt use variables for sizing, colours and z-index

For any project of size, setting variables for sizing, colours and z-index is essential.

UIs are typically based upon some form of grid or sizing ratio. Therefore sizing should be based upon set sizes, and sensible delineations of those sizes. For example here is 11px based sizing and variants as variables:

$size-full: 11px;
$size-half: 5.5px;
$size-quarter: 2.75px;
$size-double: 22px;
$size-treble: 33px;
$size-quadruple: 44px;

For a developer, the use of variables offers additional economies. For example, it saves colour picking values from composites. It also helps to normalise designs.

For example, if a project uses only 13, 15 and 22px font sizes and a change comes through requesting 14px font-sizing, the variables provide some normalisation reference. In this case, should the fonts be 13 or 15px as 14 is not used anywhere else? This allows developers to feedback possible design inconsistencies to the designers.

The same is true of colour values. For example, suppose we have a variable for the hex #333. We can write that as a variable like this:

$color-grey-33: #333;

On the surface it seems ridiculous to write the variable name when the hex value is shorter. However, again, using variables prevents unwanted variants creeping in to the code base (e.g. #323232) and helps identify 'red flags' in the code.

It's also important to still use the variables when making amendments to colours. Use colour functions on the variables to achieve your goal. For example, suppose we want a semi-opaque #333 colour.

That should be achieved in the authoring style sheets like this:

.ip-Header {
    background-color: color($color-grey-33 a(.5));
}

PostCSS can provide a polyfill for the W3C colour functions: https://drafts.csswg.org/css-color/#modifying-colors and the example above yields this CSS:

.ip-Header {
    background-color: rgba(51, 51, 51, 0.5);
}

In this example we have used the alpha CSS colour function. We use the color() function, pass in the colour we want to manipulate and then the manipulation (alpha in this instance).

Using the variables can initially seem more complex but makes it easier for future authors to reason about what colour is being manipulated.

The use of variables for z-index is equally important. This enforces some sanity when it comes to stacking contexts. There should be no need for z-index: 999 or similar. Instead, use one of only a few defaults (set as variables). Here are some relevant variables for z-index:

$zi-highest: 50;
$zi-high: 40;
$zi-medium: 30;
$zi-low: 20;
$zi-lowest: 10;
$zi-ground: 0;
$zi-below-ground: -1;

6. Thou shalt always write rules mobile first (avoid max-width)

For any responsive work, we want to embrace a mobile-first mentality in our styles. Therefore, the properties and values within the root of a rule should be the properties that apply to the smallest viewports (e.g. mobile). We then use media queries to override or add to these styles as and when needed.

Consider this:

.med-Video {
    position: relative;
    background-color: $color-black;
    font-size: $text13;
    line-height: $text15;
    /* At medium sizes we want to bump the text up */
    @media (min-width: $M) {
        font-size: $text15;
        line-height: $text18;
    }
    /* Text and line height changes again at larger viewports */
    @media (min-width: $L) {
        font-size: $text18;
        line-height: 1;
    }
}

That would yield this CSS:

.med-Video {
  position: relative;
  background-color: #000;
  font-size: 13px;
  line-height: 15px;
}

@media (min-width: 768px) {
  .med-Video {
    font-size: 15px;
    line-height: 18px;
  }
}

@media (min-width: 1200px) {
  .med-Video {
    font-size: 18px;
    line-height: 1;
  }
}

We only need to change the font-size and line-height at different viewports so that is all we are amending. By using min-width (and not max-width) in our media query, should the font-size and line-height need to stay the same at a larger size viewport we wouldn't need any extra media queries. We only need a media query when things change going up the viewport size range. To this ends, the use of max-width as the single argument of a media query is discouraged.

Bottom line: write media queries with min-width not max-width. The only exception here is if you want to isolate some style to a middle range. For example between medium and large viewports. Example:

.med-Video {
    position: relative;
    background-color: $color-black;
    font-size: $text13;
    line-height: $text15;
    /* Between medium and large sizes we want to bump the text up */
    @media (min-width: $M) and (max-width: $L) {
        font-size: $text15;
        line-height: $text18;
    }
}

7. Use mixins sparingly (and avoid @extend)

Avoid the temptation of abstracting code into mixins. There are a few areas where mixins are perfect. The code for CSS text truncation (e.g. @mixin Truncate) or iOS style inertia scrolling panels, where there are number of pseudo selectors to get right for different browsers. Another good use case can be complex font stacks.

Font stacks are difficult to get right and tedious to author. The sanest way to deal with fonts is to have the body use the most common font stack and then only override this with a different font-stack as and when needed.

For example:

.med-Video {
    position: relative;
    background-color: $color-black;
    font-size: $text13;
    line-height: $text15;
    /* At medium sizes we want to bump the text up */
    @media (min-width: $M) {
        @mixin FontHeadline;
        font-size: $text15;
        line-height: $text18;
    }
}

Mixins are great for more complex font stacks, where it's preferable to have certain font stacks apply in different situations. For example, perhaps even for the body text, one font is required for LoDPI, and another for HiDPI.

These situations can't be dealt with by using a variable alone so a mixin is used as needed.

Ultimately, aim for ten or less mixins in a project. Any more than that and it's probable mixins are being abused to needlessly abstract code.

Avoid @extends

I first came across @extend when using Sass. The @extend directive makes one selector inherit the styles of another selector. While this can offer some file size benefits it can make debugging more difficult as rules are combined together in a manner that is not always possible to predict at the point of authoring.

To determine whether using @extend was worthwhile I did a short experiment on a Sass codebase I was working with. There were 73 instances that would require a Headline font stack and 37 instances that would require a Headline Condensed font stack (so if going the mixin route, that's 73 @include Headline and 37 instances of @include HeadlineCondensed).

Using @includes

Using mixins (@includes in Sass) for the 'Headline' and 'Headline Condensed' the file size of the resultant CSS was:

146.9kB (minified), 15.4kB (Gzipped)

With NO @includes

With no font declarations AT ALL (e.g. all instances of @include Headline and @include HeadlineCondensed were simply removed):

105.5kB (minified), 14.2kB (Gzipped)

A large saving in raw file size but when Gzipped, Just over a 1KB saving.

Using @extend

By using an @extend rather than an @include:

106.9kB (minified), 14.5 (Gzipped)

What to conclude from this data? For me, all other things being equal, if you absolutely want the smallest file size, perhaps @extend is the way to go. There is some saving, albeit minor. However, being realistic, if there is any maintainability gain for you using @include instead of @extend I certainly wouldn't worry about the file size. Personally, I don't allow @extend functionality in projects. It adds an additional layer of complexity to debugging for very little gain.

8. Thou shalt comment all magic numbers and browser hacks

The variables file should contain all variables relevant to the project.

If a situation arises where a pixel based value needs entering into the authoring style sheets that isn't already defined in the variables this should serve as a red flag to you. This scenario is also covered above. In the case where a 'magic' number needs entering in the authoring style sheets, ensure a comment is added on the line above to explain its relevance. This may seem superfluous at the time but think of others and yourself in 3 months time. Why did you add a negative margin of 17 pixels to that element?

Example:

.med-Video {
    position: relative;
    background-color: $color-black;
    font-size: $text13;
    line-height: $text15;
    /*We need some space above to accommodate the absolutely positioned icon*/
    margin-top: 20px;
}

The same goes for any device/browser hacks. You may have your syntax but I use a comment above the start of the hack code with the prefix /*HHHack:*/ when I have to add code purely to satisfy a particular situation. Consider this:

.med-Video {
    background-color: $color-black;
    font-size: $text13;
    line-height: $text15;
    /*HHHack needed to force Windows Phone 8.1 to render the full width, reference ticket SMP-234 */
    width: 100%;
}

These kinds of overrides should be bottom-most in the rule if at all possible. However, make sure you add a comment. Otherwise, future authors may look at your code and presume the line(s) are superfluous and remove them.

9. Thou shalt not place inline images in the authoring style sheets

While we continue to support HTTP based users (as opposed to HTTP2) the practice of inlining assets provides some advantages; primarily it reduces the number of HTTP requests required to serve the page to the user. However, placing inline assets in the authoring style sheets is discouraged.

Consider this:

.rr-Outfit {
    min-height: $size-quadruple;
    background-image: url(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAA3NCSVQICAjb4U/gAAAAw1BMVEX///8AAACEhIQAAABYWFgAAAAAAAAAAAB6enomJiYAAAAiIiJ4eHgAAABiYmJgYGAAAACJiYkAAAC0tLR/f3/IyMjHx8e/v7+vr6+fn5+ZmZmUlJSBgYFmZmYiIiIQEBAAAAD////w8PDv7+/i4uLh4eHf39/W1tbS0tLR0dHMzMzHx8fFxcXCwsK/v7+4uLi0tLSvr6+rq6ulpaWfn5+ZmZmQkJCPj498fHxwcHBgYGBAQEAwMDAgICAfHx8QEBAAAAAphWZmAAAAQXRSTlMAESIiMzNEVWZmZneIiKqqu8zM3d3u7u7u7u7u7u7u7u7//////////////////////////////////////////wP/q/8AAAAJcEhZcwAACxIAAAsSAdLdfvwAAAAVdEVYdENyZWF0aW9uIFRpbWUAMjEvNS8xMpVX8IQAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzVxteM2AAAAw0lEQVQYlU2P6VLCUAyFD1KvWFQ2F1ChnLLW2oIs1ZZC8/5PRW4rM+RH5uSbLCeARt2YO2Pq+I+aafUjRs+PplbVzfeQcbyZzoZuSZoBdyKSkQxdOx+SuQKZK/m8BZ7Iia3lT8m6BQy4/NodpYjpb9OiC3i/U+1NfE08iAdIamXACggwzq7BCGgfrIxt8pOsDbjpRu/k+Q/DBWf3auSVq/1Rz55074d16gT0i8pq4JTPOC9MRIqEbzeXfx860eS71yj1GWENHluGjvJwAAAAAElFTkSuQmCC);
}

How is a future author supposed to reason about what that asset is?

Instead, let the tooling inline the image for you. This means the authoring style sheets can provide a clue as to what the image might be but also enables that image to be more easily swapped out. If employing the postcss-assets plugin, you can inline images with the inline command. Here's that prior example re-written correctly:

.rr-Outfit {
    min-height: $size-quadruple;
    background-image: inline("/path/to-image/relevant-image-name.png");
}

10. Thou shalt not write complicated CSS when simple CSS will work just as well

Try and write CSS code that is as simple as possible for others to reason about in future. Writing loops, mixins and functions should be minimised or used very seldom. As a general rule, if there are less than 10 variations of a rule, write it 'by-hand'. If on the other hand you need to create background positions for a sprite sheet of 30 images, this is something that tooling should be used for.

This pursuit of simplicity should be extended in the manner layouts are achieved. If a better supported layout mechanism achieves the same goal with the same amount of DOM nodes as a less well supported one, use the former. However, if a different layout mechanism reduces the number of DOM nodes needed or presents additional benefits yet is simply unfamiliar (I'm thinking of you Flexbox), take the time to understand the benefits it might offer.

Summary

Rules are nothing without enforcement. When many hands are touching the CSS codebase, no amount of education, strong words or documentation can prevent the quality of your codebase getting diluted. Alongside the aforementioned 'carrots', it's therefore necessary to provide a little 'stick'.

In this case, the 'stick' will take the form of static analysis 'linting' tools that can check and enforce code quality as authors write. This approach can prevent non-con-formant code ever making it further than the offending developers local machine. In the next chapter we will look at how to approach that, alongside tooling in general.

Here come the 'Fun Police'!