Book a Consultation

Cyclomatic Complexity: Logic in CSS

Written by on CSS Wizardry.

Table of Contents
  1. Cyclomatic Complexity

For the longest time, we’ve been saying that CSS doesn’t have logic. By that, we meant that there was no control flow or way of programmatically manipulating it. This inherent lack of logic has been used as an argument in favour of using preprocessors (to provide that missing feature), and as an argument against using preprocessors (CSS was never meant to have logic, so we shouldn’t go introducing it).

However, I recently hit upon a way of thinking that made me realise that CSS does include logic, and the fact that it’s rarely viewed as such is probably also why we end up with such poor CSS at all.

I found myself explaining compound selectors to a client as being made up of the subject—the thing we’re actually interested in—and its conditions. For example:

div.sidebar .login-box a.btn span {
}

In this compound selector, the subject is span, and the conditions are IF (inside .btn) AND IF (on a) AND IF (inside .login-box) AND IF (inside .sidebar) AND IF (on div).

That is to say, every component part of a selector is an if statement—something that needs to be satisfied (or not) before the selector will match.

This subtle shift in the way we look at how we write our selectors can have a huge impact on their quality. Would we really ever write (pseudo code):

@if exists(span) {

  @if is-inside(.btn) {

    @if is-on(a) {

      @if is-inside(.login-box) {

        @if is-inside(.sidebar) {

          @if is-on(div) {

            # Do this.

          }

        }

      }

    }

  }

}

Probably not. That seems so indirect and convoluted. We’d probably just do this:

@if exists(.btn-text) {

  # Do this.

}

Every time we nest or qualify a selector, we are adding another if statement to it. This in turn increases what is known as its Cyclomatic Complexity.

Cyclomatic Complexity

In software engineering, Cyclomatic Complexity is a metric which concerns itself with the number of ‘moving parts’ in a piece of code. These moving parts are usually points within some control flow (if, else, while, etc.), and the more of them we find, the greater our Cyclomatic Complexity. Naturally, we want to keep our Cyclomatic Complexity as low as possible, as greater complexity means that, among other things,

  • code is harder to reason about;
  • there are more potential points of failure;
  • code is harder to modify, maintain, or reuse;
  • you’re left with more outcomes and side effects to be aware of;
  • and code is more difficult to test.

Applied to CSS, we’re basically looking at the number of decisions a browser has to make before it can or cannot style something. The more if statements in our selectors, the greater the Cyclomatic Complexity that selector has. This means our selectors are more fragile, because they have a greater number of conditions that must be satisfied in order for them to work at all. It means our selectors are less explicit, because introducing if statements unnecessarily can lead to false positive matches. It makes our selectors far less reusable, because we’re making them jump through way, way more hoops than they need to.

So Instead of binding onto a span inside of a .btn (and so on) we would be far better off creating a new class of .btn-text to bind onto. This is far more direct, as well as being more terse and more robust (more @ifs lead to more brittle selectors that have a greater chance of breaking).

The trick is to write your selectors how the browser parses them: right to left. If your first question is is there a span? then your net is being cast far too wide. This is why you then have to qualify the span with so many conditionals: to narrow the selector’s reach. You need to start at the other end; write something unambiguous and explicit and forgo the conditions entirely.

Instead of your selectors casting a really wide net that catch way too much of the DOM—and then having to trim that catch down via conditions—it is far more succinct and robust to just catch much less of the DOM in the first place.

Cyclomatic Complexity is quite an advanced principle to try and apply to CSS, but if we look at it as just that—a principle—we can start to visualise and even measure the complexity in the ‘logic’ powering our selectors, and we can then begin making much better decisions based on it.

Some good rules of thumb:

  • Think of your selectors as mini programs: Every time you nest or qualify, you are adding an if statement; read these ifs out loud to yourself to try and keep your selectors sane.
  • Keep your Cyclomatic Complexity to a minimum: Use a tool like Parker to actually get metrics about your selectors’ mean Cyclomatic Complexity (Identifiers Per Selector).
  • If you don’t need the checks, don’t put them in there: Sometimes nesting in CSS is necessary, but most of the time it is not. Even the Inception Rule should not be trusted.
  • Start thinking about your selectors from the right first: Start with the bit you know you want and then write as little extra CSS as possible in order to get a correct match.
  • Brush up on Selector Intent: Make sure you’re writing the selectors you intend, not just the ones that happen to work.

Your selectors are the most fundamental aspect of your CSS architecture; make sure you’re writing sane and simple ones.


By Harry Roberts

Harry Roberts is an independent consultant web performance engineer. He helps companies of all shapes and sizes find and fix site speed issues.



Did this help? We can do way more!


Hi there, I’m Harry Roberts. I am an award-winning Consultant Web Performance Engineer, designer, developer, writer, and speaker from the UK. I write, Tweet, speak, and share code about measuring and improving site-speed. You should hire me.

You can now find me on Mastodon.


Suffering? Fix It Fast!

Projects

  • inuitcss
  • ITCSS – coming soon…
  • CSS Guidelines

Next Appearance

Learn:

I am available for hire to consult, advise, and develop with passionate product teams across the globe.

I specialise in large, product-based projects where performance, scalability, and maintainability are paramount.