Prefer repeating selectors over !important keyword in CSS

Published on
Updated on

Enlightenment
Enlightenment

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:

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.





Read previous



Disqus uses cookies, please check Privacy & cookies before loading the comments.
Please enable JavaScript to view the comments powered by Disqus.


UP
This site uses cookies for some services. By clicking Accept, you agree to their use. To find out more, including how to control cookies, see here: Privacy & cookies.