Using :where() To Reduce CSS Specificity Issues
This morning, I started looking into the Open Props project by Adam Argyle. And, to kick it off, I watched the video, Build Custom Interfaces Using CSS Open Props. In that video, Adam said something that gave me pause: all of the CSS selectors that he authored in Open Props base style-sheets use the CSS pseudo-class function, :where()
. This gives the selectors a specificity of 0
; which means that they can be overridden with ease in a concrete implementation. As someone who has spent years fighting against terrible decisions in a base design architecture, this novel technique was something that I had to try for myself.
Run this demo in my JavaScript Demos project on GitHub.
View this code in my JavaScript Demos project on GitHub.
If you've used CSS for long enough, you've almost certainly come up against CSS specificity issues. A specificity issue is when you go to style an HTML element with a relatively simple selector, such as:
.search
... only to find out that none of your CSS is being applied because the given element has already been styled with a CSS selector that has a higher specificity, such as:
dialog form input[type="search"]
This forces you to add wholly unnecessary segments in your CSS selector in order to try and create an even higher specificity:
dialog form input[type="search"].search
In the aforementioned video, Adam is saying that in order avoid this arms race of CSS specificity, he wraps his design system selectors in :where()
. So, as an example, he might rewrite the previous CSS selector as:
:where( dialog form input[type="search"] )
By using :where()
, the given selector always has a specificity of 0
; which means that our .search
selector, which has a specificity of 1
, can override it just as the developer would hope.
To see this is in action, I've created a small demo that uses :where()
to define some default form styles. And then, I override some of those form styles with plain element selectors:
<!doctype html>
<html lang="en">
<body>
<h1>
Using <code>:where()</code> To Reduce CSS Specificity Issues
</h1>
<form>
<label for="search">
Search Site:
</label>
<input id="search" type="search" />
</form>
<!-- Imagine this is some design system that I pulled into my app. -->
<style type="text/css">
:where( body ) {
font-family: monospace ;
}
:where( form label ) {
display: block ;
font-size: 20px ;
font-weight: bold ;
margin-bottom: 7px ;
}
:where( form input[ type = "search" ] ) {
border: 2px solid #333333 ;
color: #333333 ;
font-size: 20px ;
padding: 0.5em 0.7em ;
width: 300px ;
}
</style>
<!--
Imagine this is me trying to override some UI components in a given view. Notice
that my CSS selectors can be super simple. This is because the :where() pseudo-
class function (in the design system above) always results in a specificity of
`0`. In other words, it can always be overridden!
-->
<style type="text/css">
label {
margin-bottom: 10px ;
}
input {
border-color: fuchsia ;
color: fuchsia ;
font-weight: bold ;
}
</style>
</body>
</html>
Notice that I'm using a vanilla element selector, input
, to try and override a context-aware default style for all search inputs located within a form. Normally, this wouldn't work. However, since my base styles are defined using :where()
, my override is applied successfully:
As you can see, the search input is rendered with hawt fuchsia text and border colors, supplied by the input
selector. And, more importantly, these values are overriding the core values supplied by the :where()
-based selector.
This is very clever! I'm keen to see what other clever techniques Adam has built into this Open Props project.
Epilogue on CSS Layers
Recent browser releases support an @layer
CSS rule which can also help mitigate specificity issues. But, at the time of this writing, the :where()
function is considered "widely available" whereas the @layer
rule is only considered "newly available".
Want to use code from this post? Check out the license.
Reader Comments
I'm intrigued...and I know Jason Lengstorf (the interviewer) too! Definitely watching the video on open props today! Curious...how did open props catch your attention? This is the first I'm hearing of it.
@Chris,
I'm pretty sure that I heard Adam mention it once or twice on the CSS Podcast that he does with Una Kravets. I've had it open in a tab for many months. I only just finally started to peek into it because I've been looking into Tailwind CSS; and this felt very on-point for that kind of stuff.
@Ben Nadel,
Ahhh...the tabnag! I have a few of those open myself. Would make for interesting dev meetup conversation starters.
Tailwind CSS confounds me. I hear the hype, but I must be too dense to see why it's so hype worthy. When I look at it, I see very difficult to read and maintain, long, convoluted strings of CSS. I cannot make sense of the hype...and again... assume I'm missing something that I'm too dense to grep 🤷
@Chris,
You ever get on a video call, and someone does a screen share and they have like 90 tabs open 😱 😱
re: Tailwind CSS, I'm kind of in the same boat. I just started this course on Udemy, Tailwind CSS From Scratch. I know I could just read the documentation; but, I kind of want someone to just explain it to me and hopefully include reasons why they get so pumped about it. That said, I feel like there's a lot of overlap in the Open Props stuff; only, the Open Props stuff puts it all in the CSS files (which is where I like it).
@Ben Nadel,
Yes! I'm not 90 tabs bad, but at times I'm 20 tabs bad! Haha
I hope once you understand why they're so pumped about tailwind, you'll be able to share it with us as you always do. When I see something like
class="bg-slate-100 rounded-xl p-8 dark:bg-slate-800"
, I admire the simplicity and composability. But I hate the thought of it's maintainability and searchability. I wanted to changep-8
top-6
on anything withrounded-xl
, finding those instances would be a nightmare (in my imagination) unless I was extremely consistent and predictable in the order I added such rules (which, I wouldn't). And that's just a very simple case. It could get immeasurably worse (again...in my imagination)@Ben,
Ok, Open Props is beyond amazing! Already a huge fan. What an incredible productivity multiplier...consistent, great naming conventions, compostable, beautiful, wow...just wow!
@Chris,
Ha ha, awesome! Glad you're liking it. I feel like I need to start building something small with it to get a sense of how it works. I'm a bit overwhelmed at the sheer number of variables. But, I really do love a good set of constraints. I'll definitely be diving in soon!
This is interesting stuff and also caused me to check the status of @layer which we've wanted for similar functionality. Turns out it's implementation is much more widely supported now:
https://caniuse.com/?search=%40layer
Similarly, @container for responsive breakpoints on specific containers seems ready for use too!
Great post Ben!
@Mike,
The Layer and Container CSS work is ever-green at this point; but, the Google Baseline project is still classifying them as "Newly available". I think the cut-off from "newly" to "widely" is something like 30-months.
To be completely transparent, I used to be tied to IE11 for so long. And now, it kind of feels like the wild west again. And I'm not sure when it is safe to be using things. I appreciate that the Baseline project is at least drawing a line in the sand and saying that people have 30-months to get their act together 😛
To bring this full circle, the
:where()
selector is already considered "widely available". That said, the CSS you mentioned may only be a few months away from being classified as widely available as well.There's no doubt that we have so many tools to choose from at this point.
i knew about open props but had not noticed this technique. i applied it in my app to all Lucide icons to have a lower base size. the issue was that lucide has a base class
.lucide
which you can set in your styles to override the default styles for all icons. using tailwind, i wanted to lower the base size with the rulebut then when i applied overrides on specific icons like
className="w-2.5 h-2.5"
the
.lucide
class ahd higher specificity, and using this trick nailed itthanks for the tip and thank to Adam for making great stuff!
@Tzoor,
That's awesome - glad I was able to pass on this clever technique 💪
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →