Avatar of Geoff Graham
Geoff Graham on (Updated on )

The pseudo-select :is() in CSS allows you to write compound selectors more tersely. For example, rather than writing:

ul li,
ol li {}

We could write:

:is(ul, ol) li {}

This can make quick work of otherwise extremely verbose, complex, and error prone selectors. See:

:is(section, article, aside, nav) :is(h1, h2, h3, h4, h5, h6) {
  color: #BADA55;

/* ... which would be the equivalent of: */
section h1, section h2, section h3, section h4, section h5, section h6, 
article h1, article h2, article h3, article h4, article h5, article h6, 
aside h1, aside h2, aside h3, aside h4, aside h5, aside h6, 
nav h1, nav h2, nav h3, nav h4, nav h5, nav h6 {
  color: #BADA55;

You can attach the pseudo-selector to an element like you’d expect. Like to select a specific type of element when it has either of two classes:

div:is(.cool, .fun) {
  color: red;

<div class="cool">match</div>
<div class="fun">match</div>
<p class="fun">not match</p>

Hey, isn’t that like CSS preprocessing?

Simplifying selectors with :is() is similar to how CSS preprocessors handle nested rules:

/* SCSS written like this: */
div, p, ul, ol {
  span {
    /* ... */

/* after processing becomes: */
div span, p span, ul span, ol span {
  /* ...*/

/* which is a lot like the effect of :is()! */

But beware! Preprocessors, like Sass, “unroll” your nested rules into a list of easily-understood selectors. :is() will handle specificity rules a little differently.

Specificity of :is()

According to the CSS4 Working Draft:

The specificity of the :is() pseudo-class is replaced by the specificity of its most specific argument. Thus, a selector written with :is() does not necessarily have equivalent specificity to the equivalent selector written without :is().

That means that the specificity of :is() gets auto-upgraded to the most specific item in the list of arguments provided:

/* This has higher precedence... */
:is(ol, .list, ul) li { /* ... */ }

/* ...than this, even though this is later... */
ol li  { /* ... */ }

/* ...because :is() has the weight of it's heaviest selector, which is `.list`! */

Forgiving selector lists

Normally if any part of a selector is invalid, the entire block is thrown out:

p, p::not-real {
  color: red; /* nothing will be red, as ::not-real is an invalid selector */

I’ve heard browsers might chill out on this in the future, but we aren’t there yet. If you are hoping to keep that and not have to break it into two separate blocks, is() can help because it’s “forgiving”:

:is(p, p::not-real) { /* this is fine */
  color: red;

I always think about ::selection and the ::-moz-selection vendor prefix version when I think of invalid comma-separated selectors, but…

:is(::selection, ::-moz-selection) { /* this doesn't work in Chrome for some reason 🤷‍♀️ */
  background: yellow;

Browser support

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.



Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari

To get the best support, you might look at also using :matches (with vendor-prefixed :any filling in some gaps) for the general functionality. And, :not() is another pseudo-class that can assist with matching.

What’s interesting to note is that :is() was introduced after :matches which was introduced after :any. It’s sort of like :any is being replaced by :matches which is being replaced by :is(), with the details changing along the way. Always neat to see how these things evolve.

To get maximum support for “Matches-Any” requires using a mix of the historical names, as browser handling is currently a hodgepodge of vendor-prefixes and experimental settings at this point.

/* These are deprecated, but still necessary in some browsers: */
:-moz-any(div, p) > em { /* ... */ }
:-webkit-any(div, p) > em { /* ... */ }

/* Has been replaced by :is() in CSS4, but still supported 
by some browsers with experimental features enabled */
:matches(div, p) > em { /* ... */ }

/* Latest syntax */
:is(div, p) > em { /* ... */ }


Originally, this pseudo-class was named :any() and was implemented with limited vendor-specific support:

/* Never actually worked */
:any(div, p) > em { /* ... */ }

The “Matches-Any” pseudo-class name was then changed to :matches() in early versions of the CSS4 working draft, with additional (incomplete) support being given to some browsers.

/* Sort of works */
:matches(div, p) > em { /* ... */ }

The goal of the “Matches Any” selector is to make complex groupings of selectors easier to write, which is exactly what we got and more with :is().

More information