Reducing specificity problems in CSS using BEM and Custom Properties

What is specificity?

When deciding which CSS properties to apply to an element, the browser runs a calculation based on a thing called Specificity. Specificity in CSS is calculated by the complexity of the selector which applied the property.

Whilst there are various models available which describe how it works, I think of it rather simply as ID.Class.Element which can return a fixed indicator.

A selector such as #main contains 1 ID, 0 classes, and no elements, as such it's specificity can be annotated as 1.0.0.

A selector such as div.container#main however contains 1 ID, 1 class and 1 element, so it can be annotated as 1.1.1.

Now when calculating which property to load we can compare 1.0.0 against 1.1.1 . Should the number be higher in the ID column, then it wins, should it be equal it falls to the next column, and so on. Should two selector have equal specificity then order makes the final decision.

We can see there's complexity going on, and without doubt we've all had to increase specificity [and complexity] to solve a problem.


What is BEM?

BEM is a system which helps us to name our CSS classes around a component. It stands for Block Element Modifier.

Consider a simple button with two colour options that contains an icon and text:

css
.button {
    background-color: black;
    border: 2px solid black;
    padding: 8px 16px;
    border-radius: 24px
}

.button--white {
    border-color: white;
    background-color: white;
}

.button__icon {
    fill: white;
}

.button__text {
    color: white;
}

.button--white .button__icon {
    fill: black;
}

.button--white .button__text {
    color: black;
}

In this example we've got the following BEM components:

  • BLOCK - .button

  • ELEMENT - .button__icon, .button__text

  • MODIFIER - .button--white

in order to make this actually work we're having to cascade changes from the modifier into the elements, and we're able to calculate the specificity on these classes and understand the complexity:

  • .button = 0.1.0

  • .button--white = 0.1.0

  • .button__icon = 0.1.0

  • .button__text = 0.1.0

  • .button--white .button__icon = 0.2.0

  • .button--white .button__text = 0.2.0


Using CSS custom properties to reduce the BEM specificity

Now enter the scene, CSS custom properties. Most people have been using these to provide top-down ShadowDOM piercing design tokens to theme their applications globally. But they do so much more than that, they're computed at the block level using the specificity calculation.

The "target" specificity in BEM is 0.1.0, that's just enough to style an element but not to increase complexity.

Now assuming we refactor our code to remove any selectors which have a specificity greater than 0.1.0, then we can safely say that all specificity is 0.1.0 and therefore order dictates which classes are applied.

That's vital to this next part - we're going to refactor our component to have block level custom properties, that get updated by our modifiers, and cascade into our elements.

css
.button {
    --button-color: black;
    --button-contrast: white;
    background-color: var(--button-color);
    border: 2px solid var(--button-color);
    padding: 8px 16px;
    border-radius: 24px
}

.button--white {
    --button-color: white;
    --button-contrast: black;
}

.button__icon {
    fill: var(--button-contrast);
}

.button__text {
    color: var(--button-contrast);
}

Now we've refactored our components we can see that we've removed the need for two of our selectors, and maintained a 0.1.0 specificity:

  • .button = 0.1.0

  • .button--white = 0.1.0

  • .button__icon = 0.1.0

  • .button__text = 0.1.0

It also turns out that this code is also scalable, so if we wanted to add further modifiers we wouldn't need to continuously add new modifier/element overrides.

If you like this tip, you can find more examples in my github

github.com/geometricjim