How to Write Good CSS Selectors

The more you add to a CSS selector, the more precise it is, but also the more specific it is. This double edged sword is what makes writing good CSS selectors so hard. From OOCSS to BEM to atomic CSS, CSS is quickly evolving at the language level.

The :not Selectors and the Matching

The is :where and :has pseudo clauses are available for the CSS selectors themselves. Support in other browsers landed first and WebKit later as the spec matches pseudoclass. Both of those got updated in later specifications to also support complex selectors.

The :is pseudo-class in CSS 2022

The :is pseudo-class is supported in all evergreen browsers, with support only missing in IE11 Opera Mini. It makes CSS more resilient by introducing a forgiving selector list. If one of them is invalid, the entire block is discarded.

:where pseudo-class in CSS

With the :where pseudo class, libraries can ship their styling with zero specificity. :where can be applied at a selector level and doesn't require you to change. Both :is and :where are supported in all Evergreen browsers.

The :has pseudo-class

The :has pseudo-class lets us style elements by their parents without any noticeable performance implications. It's not available in any release browser yet, but it's available in Safari's technology preview and will be in 15.4.

When people talk about CSS complexity, a major contributor to that is CSS specificity, or writing effective CSS selectors.

The more you add to a CSS selector, the more precise it is, but also the more specific.

It is.

So the harder it will be to override styles if you need to, at a later point.

This double-edged sword is what makes writing good CSS selectors so hard.

You need to be specific, but not too specific.

This is why there are many strategies for writing good CSS, selectors, or avoiding writing CSS selctors altogether.

From OOCSS to BEM to Atomic CSS.

And CSS is quick evolving at the language level as well.

Something you've already learned or will learn from one of the many wonderful speakers at Hover.

Container queries and cascade layers for example, will each have their impacts on the selectors you will write in CSS, quicker than you may think, but in this talk, we're going to focus in on some new features that are available for the CSS selectors themselves.

The :is, :where, and :has pseudo classes.

First, a little bit of history, because before there was the :is pseudo class therewas the :matches pseudo class.

And before that the :any pseudo classes, :-moz-any, and :-webkit-any, and you'll be surprised to learn that the :any pseudo class has been around since 2010.

It was introduced in Firefox 4 as :moz-any.

Outside of any specification, and actually works pretty much the same as the pseudo class now, though, we'll get into that later.

Support in other browsers landed first as :-web-kit-any and later as the spec'd :matches pseudo class.

This pseudo class came with limitations though, just like the :not selector.

It only supported simple selectors.

Simple selectors are selectors that only contain a single element or element property, like class attributes, iD, or pseudo class.

As soon as you add a Combinator, like a space, tolda, or plus, or a pseudo element, you add a relation to another elements and it becomes a complex selection.

The definition of what a simple selector is changed between CSS2 and CSS3.

Where a simple selector in CSS2 now is called a "compound selector", but basically the result is the same.

The top one is simple and the bottom one is complex.

Now this puts a limit on how useful both the :matches and :not selectors are.

So luckily both of those got updated in later specifications to also support complex selectors.

Meaning we can use them to select elements based on their relations.

And that's not the only thing the :not selector contributed to this history.

The CSS working group renamed :matches to :is because, for one, it was shorter to type, but it also provided a good pairing with :not.

Is an is not.

That brings us to now, 2022.

The :is pseudo class is supported in all evergreen browsers, with support only missing in IE 11 and Opera Mini.

Now, if you want to test for support, you can use the @supports rule with the `selector` function, which has been supported for slightly longer than :is itself and works for all three pseudo classes we are discussing today.

So let's get into it.

To target multiple paths in CSS, you can add a coma between multiple selectors and each of those selectors will be used to select elements.

This is useful, but for closely related selectors, it usually leads to duplications.

Each of these selectors has duplication.

The a:hoverparts It's the same in each This is annoying, but there's something trick here when it comes to multiple selectors.

That is if one of them is invalid, the entire block is discarded.

Now you might think that it'd be pretty hard to write a selector that's invalid, maybe one that never applies, like, :not star, but an actually invalid one?

Well, consider vendor prefixes like those for inner parts of form elements.

Take this one for the range-slide-track that Josh Comeau ran into last year.

To style those in Chrome, you need -webkit-slider-runnable-track, and to style it in Firefox, you need -moz-range-track.

If you put those together, then Chrome will see the -moz-range-track selector, deem it invalid and discard the entire thing.

And likewise Firefox will see -webkit-slider-runnable-track, and discard it as.

To style this in both browsers, you would have to create two separate rule sets and duplicate the styling in both blocks-more duplication.

So quick, sidebar, Firefox doesn't actually break in this specific case.

It has support for the WebKit variants.

So more websites work in Firefox that never bother to test in it.

Make sure you test in all the browsers.

Anyway, that's two problems.

For one, duplication in selectors you write, which makes updating them frustrating.

And if one selector is invalid, everything is ignored.

Now enter :is in a superhero pose, solving both these issues with a single two letter pseudo class.

Firstly, the duplication.

Instead of creating a separate selector for each variant here, we can rewrite it to create mini selectors with the :is pseudo class.

And then we only have to write the rest of the selector once.

Do you see how we essentially create a smaller list of selectors inside the :is class?

By inlining the parts of the selector that are different we can make the entire selector much smaller and more readable.

And the :is pseudo, class solves the second issue by creating a new type of selector list-the forgiving selector list.

This will parse each selector individually and discard the ones it doesn't understand.

This means we can write :is ( ::-webkit-slider-runnable-track,::-moz-range-track) and browsers will just pick the ones they understand, and ignore the rest.

So that's great.

The :is selector saves us typing the same thing in multiple selectors.

And also made our CSSmore resilient by introducing a forgiving select list.

There is one more cool feature though, which I learned from Stephanie Eckles in her modern CSS toolkit presentation.

Because the :is pseudo class can take complex selectors, you can also add a list of ancestors and then select your elements with a star.

This means that these two selectors target the same element.

This works because the browser will look for all elements that match both `a` and that matched `nav *`. The end result being that you're actually selecting `nav a`, the a element will match both a and *. Another consequence of complex selectors being supported is that you can nest pseudo classes.

So this selector will select all navs that are in divs that do not have the class "demo".

Now a quick side note-the :not pseudo class will break with invalid selectors because its specification states that it should use a regular selctor list, not a forgiving selector list, like :is.

The workaround for that is to nest an :is selector in your :not selector though, it would be better if the specification got updated so that the :not selector also uses a forgiving selector list.

The :is pseudo class itself also comes with a few gotchas.

For one, you cannot have pseudo elements like :before and :after, since they aren't elements present in the DOM.

So if you want to style both the before and after of an element, you'll still have to write that out.

Second, the space is part of the selector.

So you have to keep spacing in mind.

For example, h1: is (: hover, : focus) will evaluate to the hover and focus applying to the h1, but adding a space between the h1 and the :is selector evaluates to the hover and focus applying to any descendants.

The universal selector is added implicitly.

The last gotcha has to do with specificity.

To determine the specificity of an :is selector, browsers don't just take the default value of a pseudo class, which is zero comma, one comma zero, but they take the value of the most specific selector in the :is pseudo class.

This means you can easily blow out your specificity without paying attention to it.

The specificity of the second selector here will end up being one comma, zero comma, zero, because of that one ID selector.

Even if that isn't actually present in your DOM.

So a quick side note on specificity.

Specificity in selectors is expressed as three numbers.

The first is for IDs, the second for classes and pseudo classes and the third for elements and pseudo elements.

Any number will prevail over the ones after it.

So you can have a hundred classes and still a single ID would win in terms of specificity.

If you want to explore this for yourself, I made an online specificity calculator that visually explains how your selector is built up and explains the scoring.

Check it out at

So what if there's no way around using a high specificity selector?

That brings us to the next pseudo class on our list-:where.

Where was introduced a little later, but is now as well-supported as :is the selector.

All evergreen browsers, but no support in IE11 or a Opera mini.

:where was created to solve this issue of specificity.

It works exactly the same as the :is pseudo class with one key difference.

Its specificity will always resolve to zero regardless of the selectors inside it.

This can help you escape out of issues where your only recourse would have been an even more specific selector or an exclamation mark important to the CSS rule.

While useful for specificity issues, there's another area where the :where pseudo class will hopefully make a big impact.

And that is in CSS, libraries and resets.

With the :where pseudo class libraries can ship their styling with zero specificity.

So you get all the benefits of a library without having to deal with specificity to overwrite its styling.

open-props is a CSS library that already applies this pattern.

So if you're interested, check out their source codes.

While cascade layers may be getting more attention lately as a potential solution for the same problem, I think the effectiveness of :where should not be underestimated.

:where can be applied at a selector level and doesn't require you to change and keep in mind the rest of your CSS architecture, the way cascade layers do.

So that's :is and :where.

These two pseudo classes help us cut down on selector duplication.

They are forgiving when it comes to invalid selectors, something that can be particularly helpful in cross browser support.

And in the case of :where, help us manage the specificity of a selector.

Both :is and :where are supported in all evergreen browsers and have been for quite a few versions.

So they are safe to use if you no longer have to support Internet Explorer, which I hope you don't.

That brings us to the last pseudo class of this talk.

It's the newest and most unsupported one, but it's the one that developers have been asking for since, well, since forever.

The parent selector.

Now, when I say parent selector, I feel the need to explain that a little bit.

With CSS selectors, you already style elements by their parents, but what people mean when they say a parent selector is that they want to style an element based on what other elements are inside of it.

For example, they want to say something like "this div image".

To specify that they want to select divs, but only if they contain images.

While there have been many proposals, this was long thought impossible or too intensive performance wise to be implemented because of the way CSS selectors are parsed, from right to left.

They start not at the beginning of a selector, but at the end.

They are parsed this way, because browser start with an element, then try and find all the CSS that applies to it.

It's faster to invalidate rules that don't apply to a given elements when you start at the ends of a selector, because you don't have to look over all potential descendants only to you find out that the element at the end doesn't match.

By starting at the end you can begin by discarding all elements that are not, in this case, `a` elements.

That lowers the total number of elements you will need to consider.

From there, you can walk up the tree one ancestor at a time to discard any further non-matching elements.

Anyway, while some veterans might now have flashbacks to old arguments about performance, jQuery plugins, and the like, it's 2022 and the :has pseudo class lets us do exactly that, natively in the browser without any noticeable performance implications.

So first the bad news.

The :has selector is not available in any released browser yet, but it's available in Safari's technology preview, will be in 15.4 and in Chromium it's available with experimental web platform features turned on.

Despite what the Can I Use data says, and Polypane, which uses Chromium, the :has selector is also already available for you to play with, along with dev tools support.

And Firefox from what I can tell has not started work on it yet.

So don't expect to be able to use this in the wild, in the near future.

When I set that there are no performance implications, what I meant was that there were no performance implications in Webkit and Blink.

implications in WebKit and Blink.

Each rendering engine has different optimizations.

So the implementation in Firefox's Gecko engine might end up being more difficult.

Of course, this won't stop us from exploring its many possibilities, because like Bramus said, the CSS pseudo class is way more than a parent selector.

In the specification it's referred to as the relational pseudo class.

And we'll see why in a bit.

First though, the syntax.

To get back to the example I just gave of selecting all divs that contain an image, here's what that looks like with :has.

We select all the divs but add a conditional that they need to have images inside them.

Like :is and :where you can also use complex selectors, for example, divs that have images as siblings, or multiple selectors, like divs that have images, videos, or SVGs as descendants.

Also like :is and :where the selector list is forgiving.

So a div:has image, video, SVG: undefined, which doesn't exist will still select all divs that have images or video elements in them The specificity of, a :has pseudo class is determined in the same way as for the :is and :not selectors.

The most specific element determines the specificity of the entire pseudo class.

You can use the :has pseudo class as part of a selector as well.

This means that you can't just select elements based on their descendants, but also select elements based on their sibling or even cousin elements.

Or in other words, we can select elements based on their relation to a different part of the DOM, the relational pseudo class.

For example, let's say you have a card element, and you want to style the h3 differently if the card's image is presence, we can do that by making :has parts of the selector that targets that h3.

To target the h3 we'd normally write `.card h3`.

Next, we add a conditional :has to the cards.

This selector matches all h3 elements that are in elements with the class card, but only if that card element also has an image in an element with the class `image-container`.

And we can also invert that- style the h3 differently if there is no image present by nesting a :not pseudo class.

This gives you a lot more styling options for situations where you would have previously added something like a `has-image` class to your card.

You can also use the :has selector to implement focus-within which lets you target elements if any of their selectors have focus.

Both of these act the same though with the :has selector, we can go one step further.

Like only styling the form if it's a specific element that has focus, like a radio button, something that's not possible with focus-within.

And this works for other pseudos as well like checked.

With this we can implement on other input fields that appears when a specific option is checked without writing a single line of JavaScript by showing the text fields when the right check box is checked.

Likewise, it will let you pull up things like the valid, invalid and user-invalid pseudo classes to style a form differently, or even to show a list of invalid or missing form fields at the top or bottom of the form.

There are more examples you can think of, but I hope this has given you a good idea of the potential that the :has pseudo selector brings.

So those are the :is, :where, and :has pseudo classes.

They help you write shorter and easier to understand CSS selectors, manage their specificity, and target elements based on their surrounding DOM structure.

:is, and :where have good browser support and :has, is making its way into browsers.

I don't think what I've shared with you today, even scratches the surface of the things folks will come up with, especially when combining these pseudo classes.

And I very much look forward to what everyone will come up with in the next couple of years and the impact it will have on how we write CSS selectors.

If you want to play around with the :has selector right now, you can use the latest Safari technology preview, Polypane, or Chrome with the experimental features flag turned on.

Thanks to the folks at Hover for letting me speak about this topic and thanks to you for watching and listening.

Enjoy the rest of Hover.

:where :is :has?

Kilian Valkhof, Web Directions AAA

CSS Complexity = CSS Selectors

div {}
div. card {}
div. card img {}
div. card imagecontainer img {}
div. card > imagecontainer img: first-child { }

Graph with label underneath reading "Precision of CSS Selector" with an arrow pointing to the right. A yellow line representing "How easy it is to overwrite" goes from top left to bottom right and a blue line representing "How specific a selector is" goes from bottom left to top right.

  • BEM
  • Atomic CSS

Strategies to deal with CSS Complexity

  • Cascade Layers
  • Container Queries
  • New pseudo classes

Manage CSS Complexity

  • :is()
  • :where()
  • :has()

New Pseudo-classes

  • :is()
  • :is()
  • :matches()
  • :is()
  • :matches()
  • :-moz-any()
  • :is()
  • :matches()
  • :-moz-any() (Firefox 4, anno 2010)

Simple selectors

div, .class, [hidden]

Simple (compound) selector:

  • p.hover#22[chat~=active]:focus

Complex selector:

  • div p

To :is() or :not() to :is()

Can I use entry for ":is CSS pseudo-class". Shows it is widely supported (in all browsers other than IE and Opera mini)

@supports (selector (:is(*))) {...}
nav ul a: hover,
footer > ol a: hover,
aside > p a: hover {
	color: purple;
	text-decoration: underline wavy deeppink;

Tweet from Josh Comeau. Text reads "Today, I solved a CSS whodunnit and learned an important lesson about the comma operator! When I launched a new blog post, sliders in Firefox were broken. I added the moz-prefixed variant, and it fixed it... but broke my sliders in Safari and Chrome! Can you tell why?"

RThe following code is then included

This works in Webkit browsers */
.slider::-webkit-slider-runnable-track {
	background: red;

/* but this DOESN'T work?? */
.slider::-moz-range-track {
	background: red;
::-webkit-slider-runnable-track { }
::-moz-range-track { }

Test in all browsers. Yes, all of them

Firefox logo

  • Duplication in the selectors makes updating harder.
  • If one selector is invalid, everything is ignored.

Two problems

Cartoon image if Superman with ":is()" as his head."

nav > ul a: hover,
footer > ol a: hover,
aside p a: hover {
	color: purple;
	text-decoration: underline wavy deeppink;
:is (nav > ul, footer ol, aside > p) a: hover {
	color: purple;
	text-decoration: underline wavy deeppink;
<forgiving- selector-list>
<forgiving- selector-list>

:is( ::-webkit-slider-runnable-track,::-moz-range-track){ }

Less duplication More resilient

The :is() pseudo class

The * selector

a: is(nav *) { ... }
nav a { ... }

Nested pseudo classes

/* all nav elements that are not in a div with class ".demo" */

: is (div: not ( . demo) nav) { ... }

:not() pseudo class

regular <selector-list>

: not (::-webkit-slider-runnable-track,::-moz-range-track) {…}

:not() pseudo class

regular <selector-list>

not (::-webkit-slider-runnable-track,::-moz-range-track) {...}


:is() gotcha's

no support for pseudo-elements

/* won't work: */
a: is (::before, ::after) { ... }
/* you have to write: * /
a::before, a::after { ... }

:is() gotcha's

Space is part of the selector

h1: is(:hover, focus) {…} /* h1:hover, h1:focus { } */ h1 : is(: hover, focus) { ... } /* h1 *:hover, h1 *:focus { } */

:is() gotcha's

The most specific selector determines the specificity

/* regular pseudo-class */
: nth-child { } /* (0,1,0) */

/* is pseudo-class * /
: is (h1, ul.nav, #message) {} /* (1,0,0) */

(id, class, element)

CSS Specificity

Screenshot of the Polypane specificity calculator

What if there’s no way around a high-specificity selector?


Can I use table for the :where selector. Shows good support in browsers other than Internet Explorer.

where (div) /* (0,0,0) *
where( .card) /* (0,0,0) * /
where(#top) / * (0,0,0) *

CSS Libraries and Resets

  • Less duplication
  • Forgiving selectors
  • Manage specificity

:is() and :where()

The parent selector

The holy grail of CSS

The following selector appears

this(div) img { ... }

An arrow labelled "Target this element" points at the this(div) part of the selector, and an arrow labelled "Not this" at the img part.

The selector "header h1#sitetitle> logo a { }" appears. An arrow labelled "starts here" points at ".logo a", while an arrow labelled "Not here" points at "header".

👴 👵

The parent selector

The holy grail of CSS

Can I use table of support for the :has() CSS relational pseudo-class. Shows support only in the most recent versions of Safari and iOS Safari, and no support in other browsers.

Browser support Soon

Icons for Safari, Chrome and Polypane, as well as a greyscale icon for Firefox.

The selector div: has (img) { ... } with an arrow labelled "target this element" pointing at the div, and an arrow labelled "not this" pointing at "img".


div: has(~ img) {}

Multiple selectors

div: has (img, video, svg) { ... }

An arrow labelled "Doesn’t exist" points at "svg: undefined" in the selector "div: has (img, video, svg:undefined) {...}"


An arrow labelled "Most specific" points at "#brand" in the selector "div: has (img logo, #brand) /* (1,0,0) */".


In the code below an arrow labelled "Style <h3> based on img being in the DOM" points at the img element

<div class="card">
	<div class=" imagecontainer">
		<img src="" alt="">
	<div class="textcontainer">
card h3 { … }
/* add a conditional to card: * /
card: has (. imagecontainer img) h3 { ... }


card: has (: not ( . imagecontainer img)) h3 {…}


form: focus-within { … } form: has (*: focus) { … }


form: focus-within { … }
form: has (*: focus) { … }
form: has (input[type=radio]: focus) { ... }

"Other" input field

form:has(input.checkbox:checked) {
	input. textfield;
	display: block;

Error styling

form:has (input:userinvalid) {
	border: 3px dashed red;

Error styling (2)

form: has ( .name: invalid),
form: has (.email: invalid) li. email-warning {
	display: block
  • Shorter and simpler CSS selectors Manage Specificity
  • Target elements based on their
  • relation to other elements

:is(), :where() & :has()

👋 Kilian Valkhof