Chapter 2. The problems of CSS at scale

In most projects, the CSS starts out with some simple rules. At the outset, you'd have to be doing something fairly daft to make maintenance of the CSS problematic.

However, as the project grows, so too does the CSS. Requirements become more complicated. More authors get involved writing the styles. Edge cases and browser workarounds need to be authored and factored in. It's easy for things to get unruly fast.

Let's consider the growing demands on a humble widget:

Before long we need to write a whole raft of overrides to a base selector. Let's consider the selectors we might need:

.widget {
    /* Base Styles */

aside#sidebar .widget {
    /* Sidebar specific */

body.home-page aside#sidebar .widget {
    /* Home page sidebar specific */

@media (min-width: 600px) {
    .widget {
        /* Base Styles 600px and above */

    aside#sidebar .widget {
        /* Sidebar specific 600px and above */

    body.home-page aside#sidebar .widget {
        /* Home page sidebar specific 600px and above */

body.product-page .widget {
    /* Product page specific */

body.product-page aside#sidebar .widget {
    /* Product page sidebar specific */

There's some basic authoring problems there if this was CSS that we wanted to scale. Let's consider some of the more obvious problems in those rules now.


The first major problem of CSS at scale is we want to by-pass the problem of specificity. Ordinarily, specificity is a useful thing. It allows us to introduce some form of logic in the CSS. Styles that are more specific than others get applied in the browser. Our example above demonstrates this: different rules will be applied in different eventualities (for example, when in the sidebar, we want to override the default styles).

Now, CSS selectors can be made up of ID, class, attribute & type selectors and any combination of those. With responsive designs you can throw media queries into the mix too.

However, not all selectors are created equal. The W3C describes how specificity is calculated here: Here is the most relevant section:

A selector's specificity is calculated as follows: count the number of ID selectors in the selector (= a) count the number of class selectors, attributes selectors, and pseudo-classes in the selector (= b) count the number of type selectors and pseudo-elements in the selector (= c) ignore the universal selector Selectors inside the negation pseudo-class are counted like any other, but the negation itself does not count as a pseudo-class. Concatenating the three numbers a-b-c (in a number system with a large base) gives the specificity.

One important thing missing there is the style attribute. Information on that elsewhere tells us that:

The declarations in a style attribute apply to the element to which the attribute belongs. In the cascade, these declarations are considered to have author origin and a specificity higher than any selector.

So, a style applied in a style attribute on an element is going to be more specific than an equivalent rule in a CSS file.

Regardless, the biggest takeaway here is that ID selectors are infinitely more specific than class based selectors. This makes overriding any selector containing an ID based selector far more difficult. For example, with a widget in the sidebar this won't work:

.widget {
    /* Widget in the sidebar */

aside#sidebar .widget {
    /* Widget in an aside element with the ID of sidebar */

.class-on-sidebar .widget {
    /* Why doesn't this work */

In this instance we would be applying a HTML class (class-on-sidebar) on the sidebar element (the aside element with the ID of sidebar) and then selecting that in the CSS lower down than the ID based selector. However, the rule still won't be applied.

Knowing what we know about specificity from the W3C specifications we can calculate the specificity of these rules.

Let's run the numbers. Left to right, the numbers after the selectors below relate to: number of inline styles, number of ID selectors, number of class selectors, and finally the number of type selectors

aside#sidebar .widget0111
.class-on-sidebar .widget0020

So you can see here that the middle selector has a greater specificity than the last. Bummer.

On a single or smaller file, this isn't that much of a big deal. We just create a more specific rule. However, if the CSS of your codebase is split across many smaller partial CSS files, finding a rule that is preventing your override from working can become an unwanted burden. Now, the problem isn't specific to ID selectors. It's more of a problem with unequally weighted selectors in the style sheets. Think of it like a heavyweight boxer pitted against a flyweight. It's not a fair contest. Creating a level playing field across the selectors used is more important than the actual selectors used.

This mis-matched soup of selectors is the crux of the specificity issue. As soon as you have a CSS codebase with hundreds of rules, any unneeded specificity starts to become a major 'pain in the ass' (that's a technical term).

So, to conclude, specificity is a problem we need to address in an ever-growing CSS codebase.

Markup structure tied to selectors

Another typical faux pas when authoring large-scale CSS is using type selectors; selectors that relate to specific markup. For example:

aside#sidebar ul > li a {
    /* Styles */

In this case we need to have an 'a' tag inside an 'li' which is a direct child of a 'ul' inside an 'aside' element with an ID of 'sidebar' - phew!

What happens if we want to apply those styles to a div somewhere else? Or any other markup structure?

We've just unnecessarily tied our rule to specific markup structure. It's often quite tempting to do this, as it can seem ridiculous to add a class to something as (seemingly) trivial as an 'a' or 'span' tag. However, I hope once you reach the end of ECSS you'll be convinced to avoid the practice.

The cascade

Typically, the cascade part of CSS is useful. Even if specificity is very equal across the selectors used, it allows equivalent rules further down the CSS file to be applied over existing rules higher up.

However, in a large codebase it becomes an unwanted safety net, allowing authors to continually add CSS to the codebase instead of amending the existing code.

This can happen for a number of reasons. As an example, authors more familiar with other languages often lack the confidence or intimate knowledge of the CSS codebase to be able to confidently remove or amend existing code. They therefore take the safe option and override existing rules using a more specific set of rules. In practical terms this means adding the new rules, with whatever selectors are necessary to get the job done, to the bottom of the existing styles.

The problem with leaning on the cascade in this way is that over time and iteration, the CSS code becomes bloated with redundant rules. The consumers of this CSS (the users) are downloading CSS full of cruft that their browser simply doesn't need.


At this point, we've covered some of the high-level problems that are symptomatic of a CSS codebase struggling to cope at scale. We've looked at the problems of specificity and the cascade. In the next chapter, we'll look at the accepted wisdom and approaches of trying to tame large CSS codebases and consider any shortcomings they present.