Skip to main content
TF
By Rohit V.8 min readArticle

CSS :is() and :where(): The Specificity Difference

TF
ToolsFuel Team
Web development tools & tips
Stylesheet code displayed on a monitor

Photo by Unsplash on Unsplash

Short Answer

> Quick answer: They do the same grouping job. The difference is specificity. `:is()` takes the specificity of its most specific argument, while `:where()` always contributes zero. Both accept a forgiving selector list, so one unsupported selector inside them gets ignored instead of killing the whole rule. Both have been available across browsers since January 2021.

Rule of thumb: use `:where()` for defaults you want anyone to override without a fight, and `:is()` when the grouped selector genuinely should carry weight.

The Problem They Were Built For

Before these existed, grouping meant repetition, and the repetition grew multiplicatively.

Writing styles for headings inside three different containers meant nine selectors, because every container had to be paired with every heading by hand. Add a fourth container and you're editing nine lines to make twelve. That's how stylesheets rot.


With `:is()` the same rule is one selector. You group the containers in one set and the headings in another, and the browser expands the combinations for you. The stylesheet stops growing faster than the design does.


Readability is the underrated half of this. A long comma separated list forces you to scan for the part that differs, while a grouped selector puts the variable part in one visible place. I've cut selector blocks down by two thirds doing nothing more than this, and the diff was easier to review afterwards.

Specificity Is the Entire Difference

Everything else about these two is the same, so this is the part worth understanding properly.

`:is()` adopts the specificity of whichever argument inside it is most specific. Put an ID in there alongside a class and the whole thing now weighs as much as an ID, even when the element actually matched via the class. That surprises people, and it's the single most common complaint about the selector.


`:where()` contributes nothing at all. Its arguments could contain an ID, three classes and an attribute selector, and the specificity added is still zero. Only whatever sits outside the `:where()` counts.


So the choice isn't stylistic. Asking which one to use is really asking whether this rule should be easy or hard to override later, and that has a real answer for any given rule. If your mental model of specificity is shaky, the
CSS units guide covers the other half of the cascade that trips people up.

The
MDN reference for :where() spells out the zero specificity rule explicitly if you want the spec language.

Why Zero Specificity Is Worth So Much

Anyone who has shipped a component library or a design system already knows the pain this fixes.

You write sensible defaults. A consumer of your library tries to override one of them, discovers your selector was more specific than theirs, and reaches for `!important`. Now their stylesheet has an escalation in it and the next override has to be worse. That spiral starts with a default that was harder to beat than it needed to be.


Wrapping the structural part of your selector in `:where()` ends it. Your defaults apply, and a single plain class from the consumer beats them cleanly because your rule contributed no specificity at all. Nobody needs `!important` and nobody has to count selector weights.


Reset stylesheets are the other obvious home for this. A reset should be the easiest thing in the cascade to override, and historically resets have been written with the same weight as real styles purely because there was no way to write them lighter.


I switched a shared component sheet over to `:where()` for exactly this reason and the `!important` count in the app consuming it dropped to almost nothing without anyone being asked to clean it up.

Forgiving Selector Lists

Both selectors share a second behaviour that gets far less attention than it deserves.

A normal comma separated selector list is all or nothing. If a single selector in that list fails to parse, whether because of a typo or because the browser doesn't know that feature yet, the entire rule is thrown away. One unsupported pseudo-class can silently delete styles that had nothing to do with it.


Inside `:is()` or `:where()`, the list is forgiving. Unrecognised selectors get dropped individually and the rest keep working. That turns a whole class of silent breakage into a non event.


The practical use is progressive enhancement. You can name a newer selector alongside an older fallback in the same rule, and older browsers keep the part they understand rather than discarding everything. Doing that with a plain list has always been a gamble.


Worth knowing this applies to the arguments inside the brackets, not to the rest of the selector. A typo outside still breaks the rule the old fashioned way.

Where People Get It Wrong

Three mistakes account for most of the confusion I see.

The first is putting an ID inside `:is()` and being surprised by the weight. If you group `#main` with `.content`, every match now carries ID level specificity. Either split them into separate rules or use `:where()` if the weight isn't wanted.


Second is assuming `:where()` makes the rule weak overall. It doesn't. It contributes zero itself, but anything outside it still counts normally, so a selector that combines a class with a `:where()` still carries that class's weight. The zero applies to the brackets, not the line.


Third is reaching for these inside deeply nested preprocessor code without checking what gets generated. Nesting plus grouping can produce selector explosions that are technically correct and genuinely slow to read. Compiled output is worth a look the first few times.


None of these are gotchas exactly. They all follow from the one rule about specificity, which is why understanding that rule is worth more than memorising patterns. Layout decisions have a similar quality, as our
flexbox versus grid guide gets into.

The Connection to CSS Nesting

Anyone using native CSS nesting is already relying on `:is()` behaviour whether they realise it or not.

Nesting computes the specificity of a nested rule as though the parent selector list were wrapped in `:is()`. That means the most specific selector in the parent list sets the weight for everything nested underneath it, exactly as it would if you had written the `:is()` by hand.


Which produces a surprise that looks like a nesting bug and isn't. Nest a rule under a parent list containing one ID and several classes, and every child rule inherits ID level weight, including the ones that only ever match through a class. People spend a long time hunting that before they find the ID sitting in the parent.


Keeping IDs out of parent selector lists avoids the whole thing. If you genuinely need an ID in the list, split it into its own rule rather than letting it set the weight for a nested block you might extend later.


Understanding this one interaction explains most of the odd specificity results people hit with nesting, and it comes straight from the same rule that governs `:is()`, which is another reason to learn the rule rather than the symptoms.

Patterns Worth Copying

A few shapes come up often enough to be worth naming.

For typography defaults inside prose blocks, wrap the container in `:where()` so any component dropped into that prose can style itself without fighting. This is the single highest value use of the selector in a real codebase.


For state variants, `:is()` is usually right. Grouping hover, focus visible and active into one rule keeps the interaction states together and you generally do want those to carry normal weight against a base style.


For theming, put the theme scope in `:where()` and the component class outside it. The theme applies, the component's own rules stay ahead of it, and switching themes doesn't require an override war.


For anything shipped to other developers, the safe default is `:where()` unless you have a reason to want the weight. Being easy to override is a feature in library code and a bug in application code, and picking based on which side of that line you're on gets the answer right nearly every time.

Frequently Asked Questions

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

Only specificity. :is() takes the specificity of its most specific argument, while :where() always contributes zero. Both group selectors the same way and both accept a forgiving selector list.

Is :where() supported in all browsers?

Yes. Both :is() and :where() have been available across browsers since January 2021, so they're safe to use without a fallback in any current project.

Why does my :is() selector override everything?

There's probably an ID inside it. :is() adopts the weight of its most specific argument, so grouping an ID with a class gives the whole selector ID level specificity even when it matched via the class.

What is a forgiving selector list?

A list where an invalid or unsupported selector is ignored instead of invalidating the whole rule. Both :is() and :where() use one, which makes them useful for progressive enhancement.

Should I use :where() in a component library?

Usually yes. Zero specificity means consumers can override your defaults with a plain class and never need !important. See the [CSS units guide](/blog/css-units-explained-px-em-rem-vh-vw) for the rest of the cascade behaviour worth knowing.

Does :where() make my whole selector zero specificity?

No, only the part inside the brackets. Anything outside the :where() still counts normally, so a class combined with a :where() still carries that class's weight.

Try ToolsFuel

23+ free online tools for developers, designers, and everyone. No signup required.

Browse All Tools