Pre-requisite
Knowledge of HTML and CSS.
A good grasp of CSS specificity and selectors.
Introduction
CSS has experienced rapid growth over the years, and a noteworthy trend highlighting this evolution is the introduction of new pseudo-classes, enhancing the overall CSS experience. Three particularly notable pseudo-classes that have gained prominence include the :has()
, :is()
, and :where()
pseudo-classes.
A good place to start is Joyce Foster's article on the :has()
pseudo-class in a comprehensive article on Open Replay, meanwhile our focus is on the remaining two selectors, which share a certain level of correlation.
This article is dedicated to exploring how the :is()
pseudo-class facilitates working with multiple selectors, and we'll also investigate the enhancements that the :where()
pseudo-class introduces to address common specificity issues. Feel free to dive right into the details.
Goals
This article caters to both experienced developers seeking to elevate their CSS skills and beginners looking to gain insights into specificity. It offers a firsthand look at how these pseudo-selectors can impact specificity, making it valuable for developers at various skill levels.
Upon reading this article, you can expect to acquire the following insights:
Develop a clear understanding of how specificity operates.
Recognize the practical utility of the
:is()
selector.Learn when to incorporate the
:where()
selector.Gain proficiency in using the
:where()
selector to resolve specificity issues.
Brushing up on Specificity
To fully grasp the content of this article, it's essential to understand how specificity is applied, as these pseudo-classes exert influence in this regard.
Specificity hinges on four parameters that dictate the weight of a selector, ultimately determining which selectors take precedence in influencing the outcome. The order of priority for these parameters is as follows: importance holds the highest weight, followed by ID, class, and HTML element.
Each parameter within a selector contributes a value of 1. A selector lacking a particular parameter is represented by a value of 0. This representation is formatted as (0,0,0,0) or (1,1,1,1), denoting the presence or absence of importance, ID, class, and HTML element, respectively.
In this context, a selector's specificity is determined by the count of 1s it possesses. For instance, when two selectors vie for the same effect, as illustrated below:
header.header-class {
/**Some CSS**/
}
header#header {
/**Some CSS**/
}
The first selector possesses a specificity rating of (0,0,1,1) as it includes both element and class selectors. The rating follows the order of importance, ID, class, and element, denoted as (important keyword, id, class, element).
The second selector holds a specificity rating of (0,1,0,1). Without an
!important
declaration and with a 1 in the second parameter, it attains a higher specificity.
This provides a high-level overview of how specificity is applied. The :is()
and :where()
pseudo-classes can either influence specificity to achieve specific effects (pun intended) or not. For a practical guide on understanding the concept of specificity, refer to W3Schools' excellent resource on specificity.
With this understanding, let's explore scenarios where these selectors prove useful. As we delve further, we'll encounter instances where both selectors impact specificity.
Ignoring errors with :is()
When chaining selectors together, it's common to encounter mistakes with invalid selectors. Consider the HTML snippet below:
<header class="flow">
<h1>The <code>:is</code> pseudo-class</h1>
<p>
A <span class="highlight">super useful</span> pseudo-class for writing less
CSS
</p>
<p>
<a href="">Read more about it here</a>, or
<a href="#">watch this video</a> where I take a look at it.
</p>
<p><a href="#" class="btn">learn more</a></p>
</header>
This snippet features a
header
element containingp
,h1
, anda
elements.Now, let's attempt to style the various elements using CSS.
header h1,
header :a {
color: olivedrab;
}
While chaining the selectors above, it's crucial to observe a mistake in the second selector,
header :a
.This error renders the CSS invalid, resulting in the inability to apply the color change to the
header
anda
HTML elements, as depicted in the image below:
Imagine making a mistake as minor as this and struggling to debug hundreds of lines of CSS due to the challenge of spotting such errors. This is precisely where the :is()
pseudo-class becomes invaluable.
Styling the same HTML with the :is()
pseudo-class enables us to better overlook these errors. Here's how to implement it:
header :is(h1, :a) {
color: olivedrab;
}
The
:is()
syntax essentially poses a question: Are the descendant or combined selectors related? In the provided CSS, it asks whetherh1
and:a
are descendants ofheader
. This is the inquiry the browser undertakes when parsing the:is()
pseudo-class.Returning to our example, we've modified the previous CSS to incorporate the
:is()
keyword. This adjustment prompts the browser to check ifh1
and:a
are descendants ofheader
and then applies the new color styling to the valid descendant.Observe in the image below the outcome of this change: the
olivedrab
color is applied to theh1
element, while no change occurs on the:a
selector because of the typing error.
This proves beneficial when dealing with extensive CSS codebases. Using :is()
allows you to identify elements that are not correctly applied and investigate those specific instances. This approach contrasts with situations where nothing works at all, providing a more manageable debugging process for large CSS projects.
Refactoring with :is()
A small yet significant advantage offered by the :is()
selector is its ability to combine selectors without unnecessary repetition, adhering to the principle of refactoring: DON'T REPEAT YOURSELF (DRY). Here are scenarios where this capability can prove helpful.
Let's begin with a card HTML sample to illustrate this:
<div class="container">
<div class="card">
<h2 class="title">Title goes here</h2>
<p>
Lorem ipsum dolor, <a href="#">sit amet consectetur</a> adipisicing elit.
Aliquid sapiente perferendis nulla sed consequuntur eveniet. Itaque
veritatis qui labore quia odio nihil, magni soluta facilis necessitatibus
cumque repellendus optio laborum.
</p>
<h3>I'm causing problems</h3>
<p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Cumque, ad!</p>
</div>
</div>
- The provided snippet is for a demonstration of a card element. Please take note of the arrangement of the class and id selectors.
Now, let's implement default styling to enhance the visual appearance of the HTML:
body {
font-family: var(--ff-primary);
min-height: 100vh;
font-size: 1.125em;
line-height: 1.6;
color: var(--clr-body);
background: var(--clr-bg);
}
.card {
padding: 3em;
box-shadow: 0 0 3em rgba(0, 0, 0, 0.15);
background: white;
}
.title {
font-weight: 900;
line-height: 0.8;
}
.container {
width: min(90%, 62.5rem);
margin: 0 auto;
}
The CSS snippet above provides styling to our HTML.
Observe in the image below the font appearance and color of all the elements, as these are the aspects that will be altered:
If, for any reason, we wish to combine selectors to modify the font appearance and colors of both elements and classes, we can augment the existing CSS as follows:
.card .title,
.card h3,
.card p {
font-family: "Trebuchet MS", "Lucida Sans Unicode", "Lucida Grande",
"Lucida Sans", Arial, sans-serif;
}
The additions above modify the font style of our selectors.
Observe the transition from the default font to the new font in the image below:
However, what if we intended to combine additional selectors? Must we repetitively type out .card
each time? Such redundancy could lead to bloated CSS code. Fortunately, with the :is()
selector, we can effortlessly achieve the same effect while minimizing repetition.
Here's how you can attain the same font change using the :is()
selector:
.card :is(.title, h3, p) {
font-family: "Trebuchet MS", "Lucida Sans Unicode", "Lucida Grande",
"Lucida Sans", Arial, sans-serif;
}
In the CSS code above, we employed the
:is()
selector to verify whether the descendant selectors are genuinely descendants of the.card
class. Consequently, there is no need to redundantly type out.card
to combine selectors, as was done previously.Observe in the image below that the font remains consistent, and we achieve the same effect:
Another clever application of refactoring CSS with :is()
is styling a descendant selector that belongs to two or more higher selectors. Let's examine some code to illustrate this.
Consider the new additions to our HTML:
<div class="container">
<header class="header-title">
<p>Welcome to Open Replay</p>
</header>
<div class="card">
<h2 class="title">Title goes here</h2>
<p>
Lorem ipsum dolor, <a href="#">sit amet consectetur</a> adipisicing elit.
Aliquid sapiente perferendis nulla sed consequuntur eveniet. Itaque
veritatis qui labore quia odio nihil, magni soluta facilis necessitatibus
cumque repellendus optio laborum.
</p>
<h3>I'm causing problems</h3>
<p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Cumque, ad!</p>
</div>
</div>
- In the HTML code above, the layout for a card remains the same, but a
header
element containing ap
element has been added.
If, for any reason, you wish to style the p
element within both the header
element and the .card
div
, you can achieve this with the :is()
selector, simplifying the process without unnecessary repetition. Here's how you would implement it:
:is(.card, header) p:hover {
color: brown;
}
In the CSS above, the browser is instructed to apply the hover effect to any
p
element within a.card
selector and aheader
element, eliminating the need to redeclarep:hover
for each element.The outcome is evident as all
p
selectors turn brown upon hovering.Observe the color change below as the cursor hovers over the
p
elements:
The problem with :is()
While the :is()
pseudo-class proves valuable for simplifying selectors and cleaning up code, it comes with a specificity concern. In the upcoming paragraphs, we will explore how the :where()
selector, closely related to :is()
, addresses specificity and examine a scenario where it can be effectively utilized.
For instance, consider the code snippets below.
The HTML markup is nearly identical to what we've been using, but with subtle changes:
<header>
<h1>The <code>:is</code> pseudo-class</h1>
<p>
A <span class="highlight">super useful</span> pseudo-class for writing less
CSS
</p>
<p>Read more about it here, or watch this video where I take a look at it.</p>
<p><a href="#" class="btn">learn more</a></p>
</header>
Here's where CSS becomes intriguing:
header :is(h1, a, .highlight) {
color: green;
}
.btn {
display: inline-block;
text-decoration: none;
padding: 0.5em 1em;
background: #234;
color: white;
}
The
:is()
selector, as expected, verifies whether theh1
,a
, and.highlight
selectors are descendants of theheader
selector.Since all descendant selectors satisfy the conditions of the
:is()
selector, all the colors change togreen
.Notably, the
btn
class has the colorwhite
and should have higher specificity, ideally being applied. In other words, the button with the text "learn more" should appear with awhite
color.However, observe in the image below that the button displays as
green
instead ofwhite
:
The problem lies in the application of the :is()
pseudo-class in the header :is(h1, a, .highlight)
selector. While it effectively groups multiple selectors together to apply a common set of styles, it doesn't impact the specificity of the individual selectors within the group.
In the provided code, the :is(h1, a, .highlight)
section matches any h1
element, any a
element, and any element with the class .highlight
that is a descendant of the header
element. The color: green;
rule is then applied to these matched elements.
Now, when it comes to the .btn
selector, it doesn't possess the same specificity as the :is(h1, a, .highlight)
selector, but it is still affected by it since it is a descendant of the header
element.
Following CSS cascade and specificity rules, the color: green;
rule from the header :is(h1, a, .highlight)
selector takes precedence over the color: white;
rule in the .btn
selector.
To address this issue, we can either enhance the specificity of the .btn
selector or make it more specific than the header :is(h1, a, .highlight)
selector. Alternatively, we can employ the :where()
selector, which we'll explore in the next section as a more straightforward solution.
Using :where()
To resolve the specificity issue highlighted in the previous section, let's implement the following changes to the CSS:
header :where(h1, a, .highlight) {
color: green;
}
.btn {
display: inline-block;
text-decoration: none;
padding: 0.5em 1em;
background: #234;
color: white;
}
In the code above, the sole modification made is substituting the
:where()
selector for the:is()
selector.This alteration achieves the desired effect, and the color of the "learn more" button reverts to white. Observe the color change in the button class below, thereby providing higher specificity to the
:where()
selector:
The issue is resolved by replacing the :is()
pseudo-class with :where()
. Although these pseudo-classes share similarities, a crucial distinction lies in how they affect specificity.
The :where()
pseudo-class is a functional pseudo-class that accepts a selector list, behaving similarly to :is()
. However, unlike :is()
, :where()
doesn't impact specificity. It functions to allow a selector list to be used without altering the specificity of the entire selector.
When using header {:where(h1, a, .highlight)}
, the styles apply to any h1
element, any a
element, and any element with the class .highlight
that is a descendant of the header
element. Importantly, the specificity of the :where()
selector doesn't interfere with the specificity of the subsequent .btn
selector. The specificity remains unchanged, akin to writing the selector without :where()
.
This feature enables grouping selectors without modifying specificity. Consequently, the color: white;
rule in the .btn
selector is not overridden by the color: green;
rule in :where(h1, a, .highlight)
. As a result, the text inside elements with the class .btn
now appears in white, as intended.
Specificity battles
If the :where()
selector negates specificity, it can prove valuable in specific scenarios, such as when establishing default values and configuring CSS for easy overrides. Let's explore an example with a card demo:
<div class="container">
<div class="card flow">
<h2 class="title">Article Topic</h2>
<p>
Lorem ipsum dolor, <a href="#">sit amet consectetur</a> adipisicing elit.
Aliquid sapiente perferendis nulla sed consequuntur eveniet. Itaque
veritatis qui labore quia odio nihil, magni soluta facilis necessitatibus
cumque repellendus optio laborum.
</p>
<h3>Foreword by Federico</h3>
<p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Cumque, ad!</p>
</div>
</div>
- The provided HTML setup is straightforward, featuring a card that nearly spans the width of the web page. Pay attention to the
flow
class, as we will analyze specificity changes originating from there.
Now, let's examine the styling for the card:
/* Other Styling */
.flow > *:not(:first-child) {
margin-top: 1em;
}
.card > h3 {
margin-top: 20em;
}
The preceding styling is conventional and doesn't impact the discussion of our example.
The
.flow > *:not(:first-child)
selector targets all direct children of elements with the class.flow
, excluding the first child. The rule applied involves adding a top margin, with the value derived from the custom property--flow-space
, and a fallback value of1em
if--flow-space
is not set.The
h3
selector targets allh3
elements, setting a fixed top margin of20em
.
However, the top margin rule remains unchanged. Why? This is due to the fact that the specificity of the .flow
selector code has elevated, leading to a conflict in determining which rules should be applied, resulting in specificity issues.
Observe in the image below that the margin-top: 20em
is not being applied, and there is no space between the text, even though space is expected:
However, by employing the :where()
selector, the margin is correctly applied. Let's see how to implement this with CSS:
.flow > :where(:not(:first-child)) {
margin-top: 0.5em;
}
.card > h3 {
margin-top: 20em;
}
- The alterations made above involve the introduction of the
:where()
selector. Observe in the image below that themargin-top
rule is appropriately applied, and the desired space is present:
In the initial snippet of this section, the :not(:first-child)
pseudo-class is employed, selecting all elements that are not the first child of their parent element. However, the :not(:first-child)
pseudo-class carries higher specificity than the .card > h3
selector. Consequently, the margin-top
value specified for .card > h3
is overridden by the more specific selector.
Conversely, in the second code snippet, the :where()
pseudo-class is utilized, which differs slightly. The :where()
pseudo-class doesn't impact specificity; it serves as a wrapper allowing you to group selectors without altering specificity. As a result, the specificity of .card > h3
is not overridden, and the margin-top
value is applied.
While this scenario may seem complex, it serves as a clear example where using the :where()
pseudo-class can help avoid prolonged debugging sessions caused by specificity conflicts.
Conclusion
The exploration of the CSS :is()
and :where()
selectors in this article has shed light on tools that enhance the efficiency and maintainability of stylesheets.
These pseudo-classes, while similar in their ability to group selectors, differ significantly in terms of specificity.
The :is()
selector proves to be a valuable asset for simplifying selectors, ignoring errors, and refactoring code.
It excels in scenarios where developers want to apply styles to multiple selectors within a group. However, its impact on specificity can lead to unexpected outcomes.
On the other hand, the :where()
selector emerges as a solution to specificity challenges. By allowing the grouping of selectors without altering specificity, it proves beneficial in scenarios where maintaining specificity is crucial, as seen in the specificity battles and the avoidance of margin issues in the final example.
Both pseudo-classes cater to developers seeking cleaner, more concise code, but careful consideration of their specificity implications is essential.
Whether it's handling errors, refactoring code, or managing specificity battles, understanding when to use :is()
or :where()
empowers developers to create more robust and maintainable stylesheets.