Back

Modern CSS selectors

Modern CSS selectors

You’re already using CSS selectors, even if you know what they are. A CSS selector helps you target a specific set of elements within your page. Some of them only target a single one, while others target a type of element, or a sub-set that you’ve marked with CSS classes. The main reason is to apply styles to the matching elements.

For instance: his basic example locates all <p> paragraph elements and changes the text color to red:

p {
  color: red;
}

And this one finds a single element and updates its padding attribute:

#my-element {
  padding: 5px;
}

In particular, pseudo-classes allow you to select elements when they enter a certain state. For instance, one that has been around since forever, is the :hover pseudo-class. Those styles will only be applied if you’re hovering over the element with your mouse. So for example, the following code underscores links when you hover over them with your mouse pointer:

a:hover {
  text-decoration: underline;
}

CSS selectors have become increasingly sophisticated since the introduction of CSS3 more than a decade ago. This tutorial discusses three recent pseudo-class selectors which target elements based on their state.

:is() Pseudo-Class Selector

(Note that older articles may refer to :matches() or :any() but the CSS standard settled on :is().)

Selectors which target different elements with the same styles can lead to verbose CSS. In this example, <p> paragraph text defaults to black but any <p> within a <main>, <header>, or <footer> is green:

/* default */
p {
  color: black;
}

/* <p> in <main>, <header>, or <footer> */
main p,
header p,
footer p {
  color: green;
}

You could use SASS and similar CSS pre-processors, which permit nesting:

main, header, footer {
  p {
    color: green;
  }
}

This creates identical CSS code and reduces typing effort but:

The :is pseudo-class provides a native CSS solution:

:is(main, header, footer) p {
  color: green;
}

:is() has full support in all modern browsers — just Internet Explorer is missing.

Any number of :is() selectors can go anywhere, and each may contain any number of other sectors. The following code colors <h1>, <h2>, and <p> elements red that are children of a <section> with a class of .primary or .secondary that is not the first child of an <article>:

article section:not(:first-child):is(.primary, .secondary) :is(h1, h2, p) {
  color: red;
}

You would require the following six CSS selectors without using :is():

article section.primary:not(:first-child) h1,
article section.primary:not(:first-child) h2,
article section.primary:not(:first-child) p,
article section.secondary:not(:first-child) h1,
article section.secondary:not(:first-child) h2,
article section.secondary:not(:first-child) p {
  color: red;
}

Note that :is() can NOT match pseudo-elements such as ::before and ::after:

/* this fails */
p:is(::before, ::after) {
  display: block;
  content: 'pseudo';
}

Check out this link with the full list of CSS pseudo-classes you can use for your selectors.

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience - start using OpenReplay for free.

:where() Pseudo-Class Selector

The :where() pseudo-class selector syntax is identical to :is(). It has the same level of browser support and will often produce the same result:

:where(main, header, footer) p {
  color: green;
}

The difference is specificity:

  • :is() uses the specificity of the most specific selector in its arguments, but
  • :where() has a specificity of zero.

Consider this HTML:

<main>
)  <p>main text</p>
</main>

The text will become green when applying the following CSS:

main p {
  color: black;
}

:is(main, header, footer) p {
  color: green;
}

:where(main, header, footer) p {
  color: blue;
}

The :is() selector has the same specificity as main p, but it comes later, so the text is green. Blue text is only applied when you remove both the main p and :is() selectors.

:where() has fewer use-cases than :is() but the zero specificity can be useful for CSS resets. Consider this reset which applies a top margin of 1em to <h2> headings unless they’re the first child of a <main>:

/* reset */
h2 {
  margin-block-start: 1em;
}

main :first-child {
  margin-block-start: 0;
}

Applying a custom <h2> top margin later in the CSS has no effect because main :first-child has a higher specificity:

/* this has no effect */
h2 {
  margin-block-start: 2em;
}

) Using the zero specificity :where()means that any reset style can be overridden without resorting to additional selectors or!important`:

/* reset */
:where(h2) {
  margin-block-start: 1em;
}

:where(main :first-child) {
  margin-block-start: 0;
}

/* this now works! */
h2 {
  margin-block-start: 3em;
}

:has() Pseudo-Class Selector

The :has() selector uses a similar syntax to :is() and :where() but targets an element which contains a set of others. That’s correct: web developers finally have a way to target parent elements — such as all <a> link anchors which contain an <img> or <div>:

/* styles applied to the <a> element */
a:has(img, div) {
  border: 2px solid blue;
}

This opens possibilities that would have required JavaScript in the past. For example, you can set the style of an outer <fieldset> when any required inner field is not valid and disable any following submit buttons:

/* red border when any required inner field is invalid */
fieldset:has(:required:invalid) {
  border: 3px solid red;
}

/* disable following submit button */
fieldset:has(:required:invalid) + button[type='submit'] {
  opacity: 0.2;
  pointer-events: none;
}

Notice the attribute selector used as part of the second style in the above example, that will select all buttons with the attribute type set to “submit”.

:has() is newer than :is() and :where(). At the time of writing, limited support is available in Safari 15.4+ and Chrome 105+, due for release in late 2022.

Conclusion

And that is it for this brief introduction to these powerful new css selectors. While they do have some very specific use cases, they’re also a great set of tools to have under your tool-belt!

Remember: the :is() and :where() pseudo-class selectors simplify CSS syntax and lessen the need for a pre-processor such as Sass.

On the other hand, :has() is more exciting. Parent selectors have been among the top CSS developers’ wishes for two decades. Still, browser vendors resisted for performance reasons (adding, removing, or modifying child elements can affect the styling of the whole page). We’ve become used to a world where parent selection was not possible, but it’ll rapidly become a popular option when most browsers support :has() in 2023.

A TIP FROM THE EDITOR: For a comparison of CSS frameworks, don’t miss our 5 CSS-in-JS Frameworks to use in 2021 article.