What are :is() and :where()

:is() and :where() are modern CSS pseudo-classes that let you write cleaner selectors by grouping multiple selector options in one place.

If you’ve ever written something like:

.card h2,
.card h3,
.card p,
.card a { ... }

You already know the pain: repetition, long selector lists, and rules that get hard to maintain.

With :is() and :where(), you can group those options inside one selector, like: .card :is(h2, h3, p, a) { ... }.

They look similar, and they match elements in a similar way, but they have a very important difference:

  • :is() has specificity based on its most specific option.
  • :where() always has zero specificity.

That specificity difference is the whole story, and it’s the reason you’ll pick one over the other.

Learn more about specificity in the CSS Specificity Interactive Tutorial.

CSS :is() basics

Think of :is() as “match if the element matches any of these selectors.”

It’s mostly used to reduce repetition when you want the same styling applied across multiple selector targets.

Grouping selectors without repeating yourself

.demo :is(h3, p, a) {
  outline: 2px dashed #111;
  outline-offset: 6px;
}
  
.demo :is(h3, p, a) {
  background: #fff5c2;
}
  
.demo :is(h3, p, a) {
  color: #111;
  text-decoration: underline;
  text-decoration-thickness: 3px;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.demo {
  font-family: system-ui, Arial, sans-serif;
  padding: 16px;
  border: 2px solid #111;
  border-radius: 12px;
  display: grid;
  gap: 12px;
  max-width: 520px;
}

.demo a {
  color: inherit;
}

.note {
  font-size: 14px;
  opacity: 0.85;
}
  

Title inside the demo

This paragraph and the link below will be targeted by :is(h3, p, a).

This is a link
This div is not targeted.

In each snippet, you’re styling multiple element types with a single selector: .demo :is(h3, p, a).

That’s the key idea: one rule, multiple selector options.

:is() works anywhere in a selector

:is() isn’t limited to element names. You can put classes, attributes, pseudo-classes, and combinations inside it.

.demo :is(.tag, [data-kind="special"], .pill) {
  background: #111;
  color: #fff;
  padding: 6px 10px;
  border-radius: 999px;
  display: inline-block;
}
  
.demo :is(.tag, [data-kind="special"], .pill) {
  background: #1b5fff;
}
  
.demo :is(.tag, [data-kind="special"], .pill) {
  background: #0a7a3f;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.demo {
  font-family: system-ui, Arial, sans-serif;
  border: 2px solid #111;
  border-radius: 12px;
  padding: 16px;
  max-width: 520px;
}

.row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
}

.small {
  font-size: 14px;
  opacity: 0.85;
}
  
.tag .pill [data-kind="special"] This one is not matched

Notice how we mixed a class, an attribute selector, and another class inside :is(). That’s totally normal.

CSS :where() basics

:where() matches exactly like :is(), but it has one superpower: it adds zero specificity.

That makes :where() perfect for “base styles” you want to be easy to override later.

A simple :where() example

.demo :where(h3, p, a) {
  background: #e9f3ff;
  padding: 4px 6px;
  border-radius: 6px;
}
  
.demo :where(h3, p, a) {
  background: #ffe9f1;
}
  
.demo :where(h3, p, a) {
  background: #eafff1;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.demo {
  font-family: system-ui, Arial, sans-serif;
  border: 2px solid #111;
  border-radius: 12px;
  padding: 16px;
  display: grid;
  gap: 10px;
  max-width: 520px;
}

.demo a {
  color: inherit;
}
  

Heading

Paragraph text

Link text

So far, it looks like :is(), right? The difference shows up when specificity gets involved.

Specificity: :is() vs :where()

Specificity decides which CSS rule wins when multiple rules match the same element.

Here’s the headline:

  • :is() takes on the specificity of its most specific selector option.
  • :where() always contributes 0 specificity, even if it contains IDs.

:is() uses the most specific option inside

In this playground, we’ll intentionally create a “battle” between rules.

.demo :is(.title, #hero-title) {
  color: #1b5fff;
}
.demo .title {
  color: #0a7a3f;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.demo {
  font-family: system-ui, Arial, sans-serif;
  border: 2px solid #111;
  border-radius: 12px;
  padding: 16px;
  max-width: 520px;
}

.title {
  font-size: 22px;
  font-weight: 700;
}
  
I have both .title and #hero-title

The first selector uses :is(.title, #hero-title). Because #hero-title is more specific than .title, the :is() selector becomes “ID-level specific”.

That means :is(.title, #hero-title) can compete with (and sometimes beat) other rules in ways you might not expect.

:where() is always zero specificity

Now we’ll do something that feels like cheating: put an ID inside :where(). It still won’t gain specificity.

.demo :where(#hero-title) {
  color: #1b5fff;
}
.demo .title {
  color: #0a7a3f;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.demo {
  font-family: system-ui, Arial, sans-serif;
  border: 2px solid #111;
  border-radius: 12px;
  padding: 16px;
  max-width: 520px;
}

.title {
  font-size: 22px;
  font-weight: 700;
}
  
I still have .title and #hero-title

Even though :where(#hero-title) looks “strong”, it isn’t. The :where() part contributes 0 specificity.

This is why :where() is perfect for baseline rules you want to be easy to override later.

Real-world patterns

Pattern: cleaner component typography

A common pattern is: “Inside my component, style headings and text consistently.”

You can do this with :where() so it’s easy for a later rule to override a single element when needed.

.card :where(h3, p) {
  margin: 0;
}

.card :where(h3) {
font-size: 20px;
}

.card :where(p) {
opacity: 0.85;
} 
.card :where(h3, p) {
  margin: 0;
}

.card :where(h3) {
  font-size: 20px;
}

.card :where(p) {
  opacity: 0.85;
}

.card p {
  opacity: 1;
  font-weight: 700;
}
  
.card :where(h3, p) {
  margin: 0;
}

.card :where(h3) {
  font-size: 20px;
}

.card :where(p) {
  opacity: 0.85;
}

.card .highlight {
  opacity: 1;
  font-weight: 700;
  text-decoration: underline;
  text-decoration-thickness: 3px;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.card {
  font-family: system-ui, Arial, sans-serif;
  border: 2px solid #111;
  border-radius: 14px;
  padding: 16px;
  max-width: 520px;
  display: grid;
  gap: 10px;
  background: #f7f7f7;
}

.card a {
  color: inherit;
}
  

Card title

Regular paragraph text.

This paragraph has a class.

The first snippet sets gentle defaults using :where(). The later snippets show how easy it is to override those defaults with a normal selector like .card p or .card .highlight.

Pattern: interactive states with :is()

Another great use of :is() is grouping states like :hover, :focus-visible, and :active.

.actions :is(a, button) {
  border: 2px solid #111;
  border-radius: 12px;
  padding: 10px 14px;
  background: #fff;
  color: #111;
  text-decoration: none;
  font: inherit;
  display: inline-block;
}

.actions :is(a, button):is(:hover, :focus-visible) {
background: #111;
color: #fff;
outline: none;
} 
.actions :is(a, button) {
  border: 2px solid #111;
  border-radius: 12px;
  padding: 10px 14px;
  background: #fff;
  color: #111;
  text-decoration: none;
  font: inherit;
  display: inline-block;
}

.actions :is(a, button):is(:hover, :focus-visible) {
  transform: translateY(-2px);
  outline: none;
}
  
.actions :is(a, button) {
  border: 2px solid #111;
  border-radius: 12px;
  padding: 10px 14px;
  background: #fff;
  color: #111;
  text-decoration: none;
  font: inherit;
  display: inline-block;
}

.actions :is(a, button):is(:hover, :focus-visible) {
  box-shadow: 0px 10px 0px 0px rgba(0, 0, 0, 0.18);
  outline: none;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  font-family: system-ui, Arial, sans-serif;
  max-width: 520px;
  padding: 16px;
  border: 2px solid #111;
  border-radius: 14px;
  background: #f7f7f7;
}

.actions {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  align-items: center;
}
  
Link action

Two nice details here:

  • :is(a, button) groups element types.
  • :is(:hover, :focus-visible) groups states.

This keeps your selectors compact without losing meaning.

In the real world, you would probably use a transition to make the hover effect smoother. Learn more in the CSS Transition Interactive Tutorial.

Pattern: component “reset” with :where()

Sometimes you want a tiny reset inside a component, but you don’t want it to be hard to override later. That’s a perfect job for :where().

.panel :where(h3, p, ul) {
  margin: 0;
  padding: 0;
}

.panel :where(ul) {
list-style: none;
display: grid;
gap: 6px;
} 
.panel :where(h3, p, ul) {
  margin: 0;
  padding: 0;
}

.panel :where(ul) {
  list-style: none;
  display: grid;
  gap: 6px;
}

.panel ul {
  list-style: disc;
  padding-left: 18px;
}
  
.panel :where(h3, p, ul) {
  margin: 0;
  padding: 0;
}

.panel :where(ul) {
  list-style: none;
  display: grid;
  gap: 6px;
}

.panel li {
  background: #fff;
  border: 1px solid #111;
  border-radius: 10px;
  padding: 8px 10px;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.panel {
  font-family: system-ui, Arial, sans-serif;
  max-width: 520px;
  border: 2px solid #111;
  border-radius: 14px;
  padding: 16px;
  display: grid;
  gap: 10px;
  background: #f7f7f7;
}

.panel h3 {
  font-size: 20px;
}
  

Panel title

A short intro line.

  • First item
  • Second item
  • Third item

The first snippet “resets” margins and padding without creating a specificity increase. The next snippets show how you can still override normally.

Forgiving selector lists

:is() and :where() use what’s called a forgiving selector list.

Translation: if one of the selectors inside is invalid, the browser can ignore that one and still use the rest.

This can be useful when experimenting with newer selectors (such as :has()), or when you want to include optional selector patterns.

.demo :is(h3, p, ???) {
  border: 2px dashed #111;
  padding: 8px;
}
  
.demo :where(h3, p, ???) {
  border: 2px dashed #111;
  padding: 8px;
}
  
.demo :is(h3, p) {
  border: 2px dashed #111;
  padding: 8px;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.demo {
  font-family: system-ui, Arial, sans-serif;
  max-width: 520px;
  border: 2px solid #111;
  border-radius: 14px;
  padding: 16px;
  display: grid;
  gap: 10px;
  background: #f7f7f7;
}

.demo h3,
.demo p {
  margin: 0;
}
  

Heading

Paragraph

In real code you wouldn’t write ???, obviously. The point is: the list is forgiving, so one wrong option doesn’t necessarily destroy the whole match.

If your selector ever “mysteriously stops working”, check for typos inside your :is() or :where() list.

Common mistakes and “not working” checklist

Mistake: expecting :is() or :where() to select a parent

:is() and :where() only help match the element they’re attached to. They do not “go upward” in the DOM.

This is valid: .card :is(h3, p) { ... }.

This is not “parent selection magic”: :is(.card) h3 { ... }

That second selector just means “an h3 inside an element that matches :is(.card)”, which is the same as .card h3.

Mistake: commas and spacing

  • Inside :is() and :where(), selectors are separated by commas.
  • Outside, it’s still normal CSS selector rules.

For example: .nav :is(a, button):is(:hover, :focus-visible) { ... }

That means: “inside .nav, match a or button, and also match hover or focus-visible.”

Debugging checklist

  • Did you put the commas in the right place? Inside the parentheses, options are comma-separated.
  • Is one of your selectors misspelled? A wrong selector option can make matching behave differently than you expect.
  • Are you surprised by specificity? If a rule “won’t override”, check whether you used :is() (can be strong) or :where() (always weak on purpose).
  • Are you targeting the right element? .card :is(h3, p) targets the h3 and p, not .card itself.
  • Is another rule later in the stylesheet winning? If specificity ties, later rules win.

When to use :is() vs :where()

  • Use :is() when you want grouping and you’re okay with normal specificity rules applying.
  • Use :where() when you want grouping but you want the rule to be easy to override.

A good way to remember it:

  • :is() is “grouping with strength”.
  • :where() is “grouping without drama”.

Browser support and resources

Both :is() and :where() are supported in modern browsers. If you need to double-check support for your audience, use these references:

Final recap

  • :is() and :where() help you group selectors and write cleaner CSS.
  • :is() keeps specificity based on the most specific option inside it.
  • :where() has zero specificity, making it ideal for baseline styles and component resets.
  • Use :is() for compact “match any of these” selectors, including grouped states like hover and focus.
  • Use :where() when you want rules that are intentionally easy to override.