DEV Community

Rasika Dangamuwa
Rasika Dangamuwa

Posted on

Why CSS Specificity Still Breaks in 2026: 5 Traps with :is(), :where(), and Cascade Layers

Every frontend developer has experienced the frustration: you write a CSS rule, inspect the DOM in DevTools, and see your styles crossed out by a selector you swore had lower specificity.

For years, the formula was simple: calculate the (a, b, c) tuple where a is IDs, b is classes/attributes/pseudo-classes, and c is type selectors. But modern CSS—specifically :is(), :where(), :has(), and Cascade Layers (@layer)—has introduced subtle mechanics that defy classical specificity math.

Here are 5 specificity traps that catch even experienced engineers in production.


1. The :is() Max-Specificity Inflation Trap

The :is() pseudo-class accepts a selector list and takes the specificity of its most specific argument, applying that weight to all matching elements—even if the element only matched a simpler branch.

Consider this selector:

:is(header, #main-nav) a {
  color: #3b82f6;
}
Enter fullscreen mode Exit fullscreen mode

You might assume that header a has a specificity of (0, 0, 2). In reality, because #main-nav is inside the :is() list, the selector :is(header, #main-nav) a has a specificity of (1, 0, 1) for every matched anchor tag.

If you later write:

header .nav-link {
  color: #10b981; /* Specificity (0, 1, 1) -> LOSES to (1, 0, 1) */
}
Enter fullscreen mode Exit fullscreen mode

The class-based selector fails to apply inside <header> because :is() inflated the specificity of the entire rule to match the ID.


2. The :where() Zero-Specificity Invisibility

The companion to :is() is :where(). While :is() takes the maximum specificity of its selector list, :where() always has a specificity of (0, 0, 0), regardless of what is inside it.

:where(#hero-banner .btn-primary) {
  padding: 12px 24px;
}
Enter fullscreen mode Exit fullscreen mode

Even though #hero-banner .btn-primary looks like (1, 1, 0), wrapping it in :where() reduces the specificity to (0, 0, 0).

A bare element selector elsewhere in your stylesheet will override it:

button {
  padding: 8px 16px; /* Specificity (0, 0, 1) -> WINS over (0, 0, 0) */
}
Enter fullscreen mode Exit fullscreen mode

:where() is incredible for writing zero-specificity CSS resets and component defaults, but using it without understanding its complete specificity erasure will lead to ghost overrides.


3. Nested Pseudo-Class Combinations (:not and :has)

In modern CSS specifications (Selectors Level 4), :not() and :has() compute their specificity based on the most specific selector in their argument list, just like :is().

/* Specificity: (1, 1, 0) because #admin is evaluated even for non-admins */
button:not(.btn-disabled, #admin) {
  cursor: pointer;
}
Enter fullscreen mode Exit fullscreen mode

When building complex UI component libraries with nested pseudo-classes, manually calculating the compound (a, b, c) weight across multiple selector branches becomes error-prone. When auditing component selector weights or debugging nested pseudo-classes, checking the raw (a, b, c) breakdown with a quick tool like the Nutilz CSS Specificity Calculator helps verify exact selector weights before pushing CSS changes.


4. Cascade Layers: Unlayered Styles Always Win

Cascade Layers (@layer) organize CSS into explicit priority buckets. The declaration order of layers determines precedence:

@layer reset, base, components, utilities;
Enter fullscreen mode Exit fullscreen mode

Here, styles in utilities override styles in components. However, there is a massive trap: unlayered styles always override layered styles, regardless of specificity.

@layer utilities {
  #app-container .modal-header.active {
    background-color: #0f172a; /* Specificity (1, 2, 0) inside @layer */
  }
}

/* Unlayered rule */
div {
  background-color: #ffffff; /* Specificity (0, 0, 1) -> WINS! */
}
Enter fullscreen mode Exit fullscreen mode

Because layer precedence is evaluated in the cascade before specificity, the unlayered div selector wins against an ID-weighted selector in @layer utilities. Specificity is only used as a tiebreaker between rules within the same layer.


5. The Layered !important Inversion Trap

The most counter-intuitive rule in CSS architecture is how !important interacts with Cascade Layers.

When you add !important to a declaration, the layer priority order reverses:

  1. Layered !important styles beat unlayered !important styles.
  2. Earlier layers with !important beat later layers with !important.
@layer base {
  p {
    font-size: 16px !important; /* WINS over utilities! */
  }
}

@layer utilities {
  .text-lg {
    font-size: 20px !important; /* LOSES to base */
  }
}
Enter fullscreen mode Exit fullscreen mode

This reversal exists by design (allowing low-level reset layers to enforce critical constraints), but if developers use !important as a quick override tool within component layers, it will backfire.


Summary Checklist for Modern CSS Specificity

  1. Group selectors with intent: Use :where() for zero-specificity defaults and :is() when you want shared specificity inheritance.
  2. Beware selector inflation: Never mix high-specificity selectors (like IDs) inside an :is() list with general element selectors.
  3. Remember layer boundaries: Specificity only resolves conflicts between rules at the same layer level. Unlayered styles always take precedence over layered rules.
  4. Avoid !important in layers: Remember that !important flips layer precedence upside down.

If you are refactoring legacy stylesheets or structuring a design system, testing your selectors in the CSS Specificity Calculator on Nutilz provides an instant visualization of ID, class, and element weights alongside specificity comparisons.

Top comments (1)

Collapse
 
topstar_ai profile image
Luis Cruz

The detail about :is() inflating specificity is a crucial reminder of how modern CSS can defy our earlier assumptions. It’s an interesting trade-off between convenience and predictable behavior that can lead to unexpected overrides. I've found that employing tools like the Nutilz CSS Specificity Calculator you mentioned can save time and confusion in debugging these issues. If you're looking for additional support in refining these CSS strategies or implementing a more robust auditing process, I’d be glad to discuss a paid collaboration. What has been your experience in educating teams about these new specificity challenges?