- Published on
CSS :is() and :where(): A Deep Dive into the Game-Changing Pseudo-Classes
- Authors
- Name
- Md Nasim Sheikh
- @nasimStg
'CSS :is() and :where(): A Deep Dive into the Game-Changing Pseudo-Classes'
Unlock cleaner, more powerful, and maintainable CSS by mastering the :is() and :where() pseudo-classes. This guide breaks down their differences, specificity rules, and practical use cases.
Table of Contents
- 'CSS :is() and :where(): A Deep Dive into the Game-Changing Pseudo-Classes'
- The Old Way: A Tangled Web of CSS Selectors
- Meet :is(): The Forgiving Selector List
- The Superpower of Forgiveness
- The Critical Detail: Specificity
- Introducing :where(): The Zero-Specificity Wonder
- Why Would You Want Zero Specificity?
- :is() vs. :where(): A Head-to-Head Showdown
- A Simple Mental Model:
- Advanced Patterns and Practical Examples
- 1. Simplifying Hover/Focus States
- 2. Styling Navigational Links
- 3. Combining with :not() for Exclusions
- Browser Support and Final Thoughts
- Conclusion
The Old Way: A Tangled Web of CSS Selectors
If you've been writing CSS for any length of time, you've undoubtedly faced this scenario: you need to apply the same style to multiple elements, but only when they're nested inside a specific container. Your CSS starts to look like a long, repetitive list of selectors.
Take this common example: you want all heading elements (h1
through h6
) inside an <article>
tag to have a particular color and font weight.
Before modern CSS, you'd write it like this:
/* The "old" repetitive way */
article h1,
article h2,
article h3,
article h4,
article h5,
article h6 {
color: #2c3e50;
font-weight: 700;
}
This works, but it's verbose and a pain to maintain. What if you want to add a .sub-heading
class to this rule? You have to append article .sub-heading
to the list. It's not scalable and it's certainly not elegant.
Even worse, if you make a typo or use an invalid selector anywhere in that list (for example, a browser-specific pseudo-element that isn't supported everywhere), the entire rule is thrown out by the browser. The whole block of CSS fails silently.
For years, this was just the cost of doing business in CSS. We relied on preprocessors like Sass or Less to manage this complexity. But what if I told you there's a native, powerful, and elegant solution baked right into CSS?
Enter :is()
and :where()
. These two functional pseudo-classes are here to revolutionize how we write complex selectors, making our stylesheets cleaner, more resilient, and infinitely more readable.
:is()
: The Forgiving Selector List
Meet The :is()
pseudo-class is your new best friend for simplifying complex selector lists. It takes a list of selectors as its argument and selects any element that can be described by any of the selectors in that list. It's like saying, "Select an element if it is this, OR this, OR that."
Let's refactor our previous example using :is()
:
/* The modern, concise way with :is() */
article :is(h1, h2, h3, h4, h5, h6) {
color: #2c3e50;
font-weight: 700;
}
Look at that! One clean, readable line. We're telling the browser to find an <article>
element, and then inside it, find any element that :is()
an h1
, h2
, h3
, and so on. It's a massive improvement in developer experience.
The Superpower of Forgiveness
Remember how a single invalid selector in a traditional list would break the entire rule? :is()
solves this beautifully. It has a feature called forgiveness. If you include an invalid or unsupported selector within the :is()
list, the browser simply ignores the invalid part and continues to apply the rule to the valid selectors.
Consider this example where we include a hypothetical, non-existent pseudo-element ::-webkit-magic-wand
:
/* Traditional (and broken) way */
header a,
footer a,
::-webkit-magic-wand a { /* This invalid selector breaks the whole rule */
color: hotpink;
}
/* The forgiving :is() way */
:is(header, footer, ::-webkit-magic-wand) a {
color: steelblue;
}
In the first block, none of the links will be hotpink
because the presence of ::-webkit-magic-wand
invalidates the entire selector group. In the second block, however, the browser will ignore the invalid selector and correctly apply color: steelblue
to links inside the <header>
and <footer>
. This makes your CSS more robust and future-proof.
The Critical Detail: Specificity
This is the most important concept to grasp about :is()
. It's not just a grouping tool; it has a significant impact on specificity. The rule is simple but powerful:
The specificity of an
:is()
pseudo-class is the specificity of its most specific argument.
Let's break this down. Specificity is how browsers decide which CSS rule applies if multiple rules point to the same element. It's often calculated as a three-part value: (ID, CLASS, TYPE)
.
#my-id
: (1, 0, 0).my-class
: (0, 1, 0)p
: (0, 0, 1)
Now, let's see how :is()
affects this. Imagine we want to style a <span>
that's inside a link with an ID of #main-nav
or just inside any header
.
/* Let's analyze the specificity */
:is(#main-nav, header) a span {
font-weight: bold;
}
How does the browser calculate the specificity here? It looks inside :is()
:
- The specificity of
#main-nav
is (1, 0, 0). - The specificity of
header
is (0, 0, 1).
The most specific selector is #main-nav
. Therefore, the entire :is(#main-nav, header)
part takes on the specificity of (1, 0, 0).
The total specificity for the rule is:
is(#main-nav, header)
-> (1, 0, 0)a
-> (0, 0, 1)span
-> (0, 0, 1)- Total Specificity: (1, 0, 2)
This means the rule is as specific as if you had written #main-nav a span
. This can be a double-edged sword. It's powerful, but it can also unintentionally escalate specificity, making your styles harder to override later. This is where its sibling, :where()
, comes into play.
:where()
: The Zero-Specificity Wonder
Introducing At first glance, :where()
looks identical to :is()
. It takes a forgiving selector list as an argument and matches elements based on that list. The syntax is the same.
Let's rewrite our heading example with :where()
:
/* Using :where() */
article :where(h1, h2, h3, h4, h5, h6) {
color: #2c3e50;
font-weight: 700;
}
Functionally, this does the same thing as the :is()
version. It selects all the headings inside an <article>
. So what's the difference? It all comes down to one groundbreaking feature:
The specificity of a
:where()
pseudo-class is always zero.
That's right. Zero. Zilch. Nada. No matter how specific the selectors are inside the list—even if you put an ID in there—:where()
contributes nothing to the overall specificity of the rule.
Why Would You Want Zero Specificity?
This might seem strange at first. Why go to the trouble of selecting something only to give it no weight? The answer is for creating sensible defaults that are incredibly easy to override.
This makes :where()
the undisputed champion for things like:
CSS Resets: Modern CSS resets (like Andy Bell's or Josh Comeau's) use
:where()
extensively. They can set base styles for elements without adding any specificity, meaning you can override them with a simple type or class selector without needing!important
or overly complex selectors.Old Reset (adds specificity):
/* This rule has a specificity of (0,0,1) */ img { max-width: 100%; display: block; }
To override this for a specific image, you'd need at least a class selector (
.logo-img
).Modern Reset with
:where()
(zero specificity):/* This rule has a specificity of (0,0,0) */ :where(img) { max-width: 100%; display: block; }
Now, even a simple
img { max-width: 50px; }
in your component stylesheet could override this, because a type selector (0,0,1) is more specific than zero (0,0,0).Theming: You can define base theme styles with
:where()
so that users or other developers can easily apply their own customizations without fighting specificity wars.Component Libraries: A library author can provide unopinionated base styles for components using
:where()
. The consumer of the library can then easily style the components to match their application's design system.
Let's revisit our specificity example, but this time with :where()
:
/* Let's analyze the specificity with :where() */
:where(#main-nav, header) a span {
font-weight: bold;
}
The specificity calculation is now completely different:
:where(#main-nav, header)
-> (0, 0, 0) (Even with an ID!)a
-> (0, 0, 1)span
-> (0, 0, 1)- Total Specificity: (0, 0, 2)
This rule is now only as specific as a span
. It's incredibly easy to override, for example, with .user-avatar span { font-weight: normal; }
.
:is()
vs. :where()
: A Head-to-Head Showdown
Let's summarize the key differences in a clear format.
Feature | :is() | :where() |
---|---|---|
Functionality | Groups selectors into a forgiving list | Groups selectors into a forgiving list |
Specificity | Takes on the specificity of its most specific argument | Always has a specificity of zero |
Primary Use Case | Reducing repetition when you want the specificity. | Creating easily overridable defaults and base styles. |
A Simple Mental Model:
Use
:is()
when you're saying: "I want to apply this style to these elements, and I want this rule to be as strong as my most specific selector." Perfect for applying styles within a specific, controlled context.Use
:where()
when you're saying: "I want to apply this style as a baseline suggestion, and I want anyone to be able to easily override it without any hassle." Perfect for resets, generic defaults, and foundational styles.
Advanced Patterns and Practical Examples
Now that we understand the fundamentals, let's explore some more advanced and practical ways to use these pseudo-classes.
1. Simplifying Hover/Focus States
Instead of writing separate rules for :hover
and :focus
, you can combine them cleanly.
/* Before */
.btn:hover,
.btn:focus {
background-color: #3498db;
color: white;
}
/* After with :is() */
.btn:is(:hover, :focus) {
background-color: #3498db;
color: white;
}
This also works beautifully for :focus-visible
and :focus-within
.
2. Styling Navigational Links
Imagine you have links in your main navigation and your footer navigation that need similar styling, especially the active link.
:is(nav, footer) a:is(:hover, [aria-current="page"]) {
text-decoration: underline;
text-decoration-thickness: 2px;
}
This single rule handles:
- A link being hovered in the
<nav>
. - A link being hovered in the
<footer>
. - A link with
aria-current="page"
in the<nav>
. - A link with
aria-current="page"
in the<footer>
.
This is incredibly expressive and powerful.
:not()
for Exclusions
3. Combining with You can create powerful logic by nesting or combining with the :not()
pseudo-class. For instance, let's add a top margin to all block-level elements, except when they are the first child.
/* Add a top margin to all these elements... */
:where(p, h1, h2, h3, ul, ol, blockquote) {
margin-top: 1.5rem;
}
/* ...but remove it if it's the first child of its parent */
:where(p, h1, h2, h3, ul, ol, blockquote):first-child {
margin-top: 0;
}
By using :where()
, we ensure these foundational layout rules have zero specificity and can be easily overridden by any component-specific styling.
Another pattern is to exclude elements with a certain class:
/* Style all headings in an article, unless they have the .no-style class */
article :is(h1, h2, h3):not(.no-style) {
border-bottom: 1px solid #eee;
padding-bottom: 0.5em;
}
Browser Support and Final Thoughts
So, can you start using these today? The answer is a resounding yes!
As of late 2023, both :is()
and :where()
have excellent support across all major modern browsers, including Chrome, Firefox, Safari, and Edge. You can check the latest data on Can I Use :is()
and Can I Use :where()
.
For projects that still require support for very old browsers (like Internet Explorer), you'd need to stick to the traditional, long-form selectors or use a PostCSS plugin like postcss-preset-env
which can transpile this modern syntax back to something older browsers understand.
Conclusion
:
is()and
:where()` are more than just syntactic sugar; they represent a fundamental improvement in how we can architect our CSS. They empower us to write code that is:
- DRY (Don't Repeat Yourself): Drastically reducing selector duplication.
- Readable: Making complex selectors easier to parse and understand at a glance.
- Maintainable: Simplifying future updates and additions.
- Resilient: Forgiving invalid selectors so that one mistake doesn't break an entire style block.
- Specificity-Aware: Giving us fine-grained control over the cascade with the high-specificity
:is()
and the zero-specificity:where()
.
By understanding the crucial difference in how they handle specificity, you can choose the right tool for the job every time. Use :is()
to group selectors when their inherent specificity is desired, and reach for :where()
to create the ultimate, easily-overridable base styles. Go forth and write cleaner, more powerful CSS!