Search code examples
csscss-selectors

What is the difference between :where() and :is()?


CSS recently added the pseudo-classes :where() and :is() and I don't understand when to use which. Both can be used to select multiple elements. Here is a snippet where the same is achieved with both pseudo-classes:

span {
  display: inline-block;
  height: 100px;
  width: 100px;
  background-color: grey;
  margin-bottom: 10px;
}

:is(.span1-1, .span1-2):hover {
  background-color: firebrick;
}

:where(.span2-1, .span2-2):hover {
  background-color: teal;
}
<span class="span1-1"></span>
<span class="span1-2"></span>
<br>
<span class="span2-1"></span>
<span class="span2-2"></span>

Can someone give an example where they behave differently from each other?


Solution

  • As mentioned, the difference is specificity. This is mentioned on MDN, though not prominently for some reason. The spec, on the other hand, is much more explicit about it:

    4.4. The Specificity-adjustment Pseudo-class: :where()

    The Specificity-adjustment pseudo-class, :where(), is a functional pseudo-class with the same syntax and functionality as :is(). Unlike :is(), neither the :where pseudo-class, nor any of its arguments contribute to the specificity of the selector—its specificity is always zero.

    What are the use cases? Well, in the example you've given, it's not terribly useful. You don't have any competing selectors for which you need to match or otherwise not override specificity. You have a basic span rule, and a :hover rule that overrides it naturally (i.e. just by how specificity works and what specificity was originally designed for). If there aren't any special or exceptional styles you need to take into account, it doesn't really matter whether you use :is() or :where().

    :where() becomes useful when you have more general rules that have unnecessarily specific selectors, and these need to be overridden by more specialized rules that have less specific selectors. Both MDN and the spec contain an example of a (very) common use case — I don't want to simply regurgitate what's on MDN, so here's the one from the spec:

    Below is a common example where the specificity heuristic fails to match author expectations:

    a:not(:hover) {
      text-decoration: none;
    }
    
    nav a {
      /* Has no effect */
      text-decoration: underline;
    }
    

    However, by using :where() the author can explicitly declare their intent:

    a:where(:not(:hover)) {
      text-decoration: none;
    }
    
    nav a {
      /* Works now! */
      text-decoration: underline;
    }
    

    Unlike MDN, the spec doesn't really explain this use case in English, so I will. The "author expectations" here are that the nav a CSS rule (what I call a specialized rule) would override the a:not(:hover) rule (what I call a general rule). Ostensibly, this does not happen.

    Because the :hover pseudo-class is more specific than type selectors, any rules that have only type selectors won't be able to override the general a:not(:hover) rule that applies to any a that isn't hovered. Traditionally you'd have needed to match the specificity of a:not(:hover), most naïvely by duplicating the offending bit:

    a:not(:hover) {
      text-decoration: none;
    }
    
    nav a, nav a:not(:hover) {
      /* Works, but not ideal, to say the least */
      text-decoration: underline;
    }
    

    Alternatively, by adding selectors that increase specificity without affecting matching:

    a:not(:hover) {
      text-decoration: none;
    }
    
    nav a:nth-child(n) {
      /* Works, but not ideal either */
      text-decoration: underline;
    }
    

    (or a:any-link, in the case of links)

    What :where() does is allow you to remove the specificity added by :hover altogether, thereby making it much easier to override this rule for certain a elements, for example by ensuring that a elements in a nav are always underlined whether or not the cursor is over them (since nav a is more specific than just a).

    Unusually, because it decreases a selector's specificity, you'd generally use :where() with selectors that need to be overridden, rather than the overriding selectors. On the other hand, you use :is() simply to reduce selector duplication in your CSS rules, e.g. by changing

    .span1-1:hover, .span1-2:hover
    

    to

    :is(.span1-1, .span1-2):hover
    

    while preserving specificity (though do note that :is() does work differently once you're grouping selectors with different specificity amounts).

    This means that while :where() has a similar syntax to :is(), in practice they're two different pseudo-classes with different use cases. Their use cases do partially overlap nevertheless. For example, you may need to reduce selector duplication in a general rule, which implies using :is(), but you'd prefer :where() instead to also reduce specificity. Since it's useful to apply a single :where() to multiple selectors, allowing it to accept a selector-list makes it so you don't have to write :where(:is(selector-list)). This principle applies to many other new pseudo-classes such as :host(), :host-context() and :has(), as well as the level 4 :not().