How the CSS :is, :where and :has Pseudo-class Selectors Work


CSS selectors allow you to choose elements by type, attributes, or location within the HTML document. This tutorial explains three new options — :is(), :where(), and :has().

Selectors are commonly used in stylesheets. The following example locates all <p> paragraph elements and changes the font weight to bold:

p {
  font-weight: bold;

You can also use selectors in JavaScript to locate DOM nodes:

Pseudo-class selectors target HTML elements based on their current state. Perhaps the most well known is :hover, which applies styles when the cursor moves over an element, so it’s used to highlight clickable links and buttons. Other popular options include:

  • :visited: matches visited links
  • :target: matches an element targeted by a document URL
  • :first-child: targets the first child element
  • :nth-child: selects specific child elements
  • :empty: matches an element with no content or child elements
  • :checked: matches a toggled-on checkbox or radio button
  • :blank: styles an empty input field
  • :enabled: matches an enabled input field
  • :disabled: matches a disabled input field
  • :required: targets a required input field
  • :valid: matches a valid input field
  • :invalid: matches an invalid input field
  • :playing: targets a playing audio or video element

Browsers have recently received three more pseudo-class selectors…

The CSS :is Pseudo-class Selector

Note: this was originally specified as :matches() and :any(), but :is() has become the CSS standard.

You often need to apply the same styling to more than one element. For example, <p> paragraph text is black by default, but gray when it appears within an <article>, <section>, or <aside>:

p {
  color: #000;

article p,
section p,
aside p {
  color: #444;

This is a simple example, but more sophisticated pages will lead to more complicated and verbose selector strings. A syntax error in any selector could break styling for all elements.

CSS preprocessors such as Sass permit nesting (which is also coming to native CSS):

article, section, aside {

  p {
    color: #444;


This creates identical CSS code, reduces typing effort, and can prevent errors. But:

  • Until native nesting arrives, you’ll need a CSS build tool. You may want to use an option like Sass, but that can introduce complications for some development teams.

  • Nesting can cause other problems. It’s easy to construct deeply nested selectors that become increasingly difficult to read and output verbose CSS.

:is() provides a native CSS solution which has full support in all modern browsers (not IE):

:is(article, section, aside) p {
  color: #444;

A single selector can contain any number of :is() pseudo-classes. For example, the following complex selector applies a green text color to all <h1>, <h2>, and <p> elements that are children of a <section> which has a class of .primary or .secondary and which isn’t the first child of an <article>:

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

The equivalent code without :is() required six CSS selectors:

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: green;

Note that :is() can’t match ::before and ::after pseudo-elements, so this example code will fail:

div:is(::before, ::after) {
  display: block;
  content: '';
  width: 1em;
  height: 1em;
  color: blue;

The CSS :where Pseudo-class Selector

:where() selector syntax is identical to :is() and is also supported in all modern browsers (not IE). It will often result in identical styling. For example:

:where(article, section, aside) p {
  color: #444;

The difference is specificity. Specificity is the algorithm used to determine which CSS selector should override all others. In the following example, article p is more specific than p alone, so all paragraph elements within an <article> will be gray:

article p { color: #444; }
p { color: #000; }

In the case of :is(), the specificity is the most specific selector found within its arguments. In the case of :where(), the specificity is zero.

Consider the following CSS:

article p {
  color: black;

:is(article, section, aside) p {
  color: red;

:where(article, section, aside) p {
  color: blue;

Let’s apply this CSS to the following HTML:

  <p>paragraph text</p>

The paragraph text will be colored red, as shown in the following CodePen demo.

See the Pen
Using the :is selector
by SitePoint (@SitePoint)
on CodePen.

The :is() selector has the same specificity as article p, but it comes later in the stylesheet, so the text becomes red. It’s necessary to remove both the article p and :is() selectors to apply a blue color, because the :where() selector is less specific than either.

More codebases will use :is() than :where(). However, the zero specificity of :where() could be practical for CSS resets, which apply a baseline of standard styles when no specific styling is available. Typically, resets apply a default font, color, paddings and margins.

This CSS reset code applies a top margin of 1em to <h2> headings unless they’re the first child of an <article> element:

h2 {
  margin-block-start: 1em;

article :first-child {
  margin-block-start: 0;

Attempting to set a custom <h2> top margin later in the stylesheet has no effect, because article :first-child has a higher specificity:

h2 {
  margin-block-start: 2em;

You can fix this using a higher-specificity selector, but it’s more code and not necessarily obvious to other developers. You’ll eventually forget why you required it:

article h2:first-child {
  margin-block-start: 2em;

You can also fix the problem by applying !important to each style, but please avoid doing that! It makes further styling and development considerably more challenging:

h2 {
  margin-block-start: 2em !important;

A better choice is to adopt the zero specificity of :where() in your CSS reset:

:where(h2) {
  margin-block-start: 1em;

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

You can now override any CSS reset style regardless of the specificity; there’s no need for further selectors or !important:

h2 {
  margin-block-start: 2em;

The CSS :has Pseudo-class Selector

The :has() selector uses a similar syntax to :is() and :where(), but it targets an element which contains a set of others. For example, here’s the CSS for adding a blue, two-pixel border to any <a> link that contains one or more <img> or <section> tags:

a:has(img, section) {
  border: 2px solid blue;

This is the most exciting CSS development in decades! Developers finally have a way to target parent elements!

The elusive “parent selector” has been one of the most requested CSS features, but it raises performance complications for browser vendors, and therefor has been a long time coming. In simplistic terms:

  • Browsers apply CSS styles to an element when it’s drawn on the page. The whole parent element must therefore be re-drawn when adding further child elements.

  • Adding, removing, or modifying elements in JavaScript could affect the styling of the whole page right up to the enclosing <body>.

Assuming the vendors have resolved performance issues, the introduction of :has() permits possibilities that would have been impossible without JavaScript in the past. For example, you can set the styles of an outer form <fieldset> and the following submit button when any required inner field is not valid:

fieldset:has(:required:invalid) {
  border: 3px solid red;

fieldset:has(:required:invalid) + button[type='submit'] {
  opacity: 0.2;
  cursor: not-allowed;

Fieldset shown with a red border and submit button disabled

This example adds a navigation link submenu indicator that contains a list of child menu items:

nav li:has(ol, ul) a::after {
  display: inlne-block;
  content: ">";

Or perhaps you could add debugging styles, such as highlighting all <figure> elements without an inner img:

figure:not(:has(img)) {
  border: 3px solid red;

Five images in a row, with a red border around the missing one

Before you jump into your editor and refactor your CSS codebase, please be aware that :has() is new and support is more limited than for :is() and :where(). It’s available in Safari 15.4+ and Chrome 101+ behind an experimental flag, but it should be widely available by 2023.

Selector Summary

The :is() and :where() pseudo-class selectors simplify CSS syntax. You’ll have less need for nesting and CSS preprocessors (although those tools provide other benefits such as partials, loops, and minification).

:has() is considerably more revolutionary and exciting. Parent selection will rapidly become popular, and we’ll forget about the dark times! We’ll publish a full :has() tutorial when it’s available in all modern browsers.

If you’d like to dig in deeper to CSS pseudo-class selectors — along with all other things CSS, such as Grid and Flexbox — check out the awesome book CSS Master, by Tiffany Brown.

Source link

About Author


Please enter your comment!
Please enter your name here

Share post:




More like this

Fixing organizational silos: 7 major challenges of the silo mentality

Turn a fragmented team into a collaborative unit...

Ensuring accuracy: What data validation is and why it matters

Data validation ensures the information you work with...

Harness conversion analysis to improve UX and unlock customer insights

Revolutionize your conversion strategy by identifying impactful clicks.While...

A Comprehensive Comparison — SitePoint

When it comes to web development, choosing the...