CSS :is() Pseudo-Class Specificity Explained

This article explains how the CSS :is() pseudo-class determines selector specificity by dynamically inheriting the weight of its most specific argument. You will learn the mechanics behind specificity calculation in CSS selectors, why matching an element via :is(.a, #b) behaves with the specificity of an ID selector regardless of which selector matches, and how this behavior affects the cascade in real-world stylesheets.

How the Specificity of :is() Works

The :is() pseudo-class simplifies complex selectors by allowing you to group multiple targets into a single rule. However, unlike traditional grouping separated by commas, the browser treats the specificity of the entire :is() expression as equal to the selector inside its argument list that possesses the highest specificity.

Specificity in CSS is calculated using three categories:

  1. A (ID selectors): e.g., #b has a score of (1, 0, 0).
  2. B (Class, attribute, and pseudo-class selectors): e.g., .a has a score of (0, 1, 0).
  3. C (Type selectors and pseudo-elements): e.g., div has a score of (0, 0, 1).

The :is() pseudo-class itself contributes zero specificity (0, 0, 0) to the calculation. Instead, the CSS parser scans the comma-separated arguments provided inside the parentheses, evaluates the specificity of each, and assigns the maximum value found to the entire :is() block.

Analyzing :is(.a, #b)

When you declare a selector such as:

:is(.a, #b) {
  color: red;
}

The browser evaluates the two arguments:

  • .a has a specificity of (0, 1, 0).
  • #b has a specificity of (1, 0, 0).

Because (1, 0, 0) is higher than (0, 1, 0), the browser sets the specificity of :is(.a, #b) to (1, 0, 0).

Crucially, this score applies statically to all matching elements, regardless of which individual selector matches the element in the DOM. Even if an element only matches the .a class and has no relation to #b, it is evaluated with the specificity of an ID selector.

Practical Example: The Specificity Trap

Consider the following HTML:

<p class="a">This text will be red.</p>

And the following CSS:

:is(.a, #b) {
  color: red;
}

p.a {
  color: blue;
}

Intuitively, you might expect p.a (specificity: 0, 1, 1) to override .a inside :is() because p.a includes both an element and a class selector.

However, the text renders as red. Because #b is included in the :is() arguments, :is(.a, #b) has a specificity of (1, 0, 0). Since (1, 0, 0) outranks (0, 1, 1), the :is() rule wins. To override this rule without an ID, you would need another selector with at least one ID or an !important declaration.

Alternative: When to Use :where()

If you need the grouping capabilities of :is() without adopting the highest specificity of the argument list, use the :where() pseudo-class instead:

:where(.a, #b) {
  color: red;
}

The :where() pseudo-class always sets its total specificity to (0, 0, 0), making it easy to override anywhere else in your stylesheet. Use :is() when you want the group to maintain a high priority in the cascade, and use :where() when you want low-specificity defaults.