Prefer repeating selectors over !important keyword in CSS
Published on
Updated on
In CSS,
!important
keyword is often used as a quick fix when some style is overridden by other style with a higher specificity. For
example:
<style>
.container .button {
background: red;
}
.blue {
background: blue;
}
.blue-important {
background: blue !important;
}
</style>
<div class="container">
<button class="button">Regular red button</button>
<button class="button blue">Still red button because of lower specificity</button>
<button class="button blue-important">Blue button</button>
</div>
However, it's likely that this approach will introduce long term problems. It will no longer be possible
to override a style with !important by simply adding a new style with !important that
has
a lower specificity selector. Eventually this will lead to an arms race of selectors. So, what should be done
instead?
Repeating selectors
Actually there is a way to increase the selector specificity without using !important. This can be
achieved by simply repeating a construct that does the same match (i.e. chaining the same selector with itself).
For example, for .blue it can be
.blue.blue. In CSS it's completely a valid thing, you just recheck the same condition. It
doesn't affect the match logic, but it increases the specificity (becomes 2 classes vs 1 class). Here is an
example of
.blue.blue in
action:
<style>
.container .button {
background: red;
}
.blue.blue {
background: blue;
}
</style>
<div class="container">
<button class="button">Regular red button</button>
<button class="button blue">Blue button, same specificity, just beats by the order</button>
</div>
You can also use
:is()
if it's not possible to simply append the selector. For example, for
tag selector like button it can be
button:is(button):
<style>
button:is(button) {
background: blue;
}
button {
background: red;
}
</style>
<button>Still blue button because of higher specificity</button>
Keep in mind that neither button:is(button), nor button:is(button):is(button), nor
even
button:is(button):is(button):is(button) will override a selector containing a class (such as
.button). No matter how many times :is(button) is repeated. This is because classes
have higher weight than tag names when the specificity is evaluated. But aside from that, this is a nice way
to control the
specificity without binding the selector to any context. The primary downside is that the size of the CSS file
might increase significantly if it's used
uncontrollably.
Also the selectors look a bit weird.
Layers can also help
There is also an alternative approach.
CSS also supports layers.
The keyword @layer lets you declare an explicit priority stack of CSS buckets, and that layer
priority is
checked before specificity and source order. Here is an example of @layer in action:
<style>
/*
layer blueButtons has the highest priority
because it is declared after layer normalButtons
*/
@layer normalButtons, blueButtons;
@layer normalButtons {
.container .button {
background: red;
}
}
@layer blueButtons {
.blue {
background: blue;
}
}
</style>
<div class="container">
<button class="button">Regular red button from layer "normalButtons"</button>
<button class="button blue">Blue button because layer "blueButtons" has higher priority</button>
</div>
However, layers also have their share of problems and quirks:
-
!importantkeyword works in a reverse way: a less specific selector with!importantin an early layer can override a more specific one in a later layer. -
Unlayered styles have higher priority than layered styles that don't use
!importanteven if the unlayered styles have lower specificity. When layered styles use!important, the situation is the opposite. - It enforces an inflexible architecture, which can later cause maintenance problems. Also (considering the other points above as well), usually you can't easily migrate to layers without huge (and painful) refactor, especially when you use third party styles.
Use :where() to lower the specificity for complex selectors
The repeated selector trick can be a good tradeoff when the existing CSS codebase uses a lot of high specificity
selectors
and the
refactor is either too risky or too hard / impossible. But for new styles you should avoid creating high
specificity selectors. The simplest way is to avoid using deep nesting and long chaining. But, if you really
need this for some reason, you can use
:where().
:where() is similar to :is(), they both do simple matching while being easily chained.
The difference is that the selector inside :where() will have a specificity of 0.
For example, :where(.container) .button will have the same specificity as plain
.button:
<style>
:where(.container) .button {
background: red;
}
.blue {
background: blue;
}
</style>
<div class="container">
<button class="button">Regular red button</button>
<button class="button blue">Blue button since
.container specificity was "consumed" by :where(),
same specificity, just beats by the order</button>
</div>
Conclusion
This simple trick of repeated selectors eliminates the need for !important in a lot of cases. It
provides a nicer way to
increase the selector specificity by minimizing long term problems. Of course, you should keep in
mind that there are some
downsides and limitations as well: bloated / weird looking selectors (if overused or overriding deep selectors)
and the impossibility to override higher weight selectors. Sometimes CSS layers can also help if repeated
selectors cause
too many issues. But, as I mentioned, they have their share of problems. And, finally, avoid writing new
selectors with higher specificity or reduce them by using :where() in order to minimize this "arms
race"
of the selectors in the future.