DEV Community

Nick Benksim
Nick Benksim

Posted on • Originally published at csscodelab.com

Advanced CSS Selectors You Might Have Forgotten

Stop Over-Engineering Your Components: The CSS Selectors You’re Probably Ignoring

Grab a coffee, let’s talk shop. You know that feeling when you’re staring at a React or Vue component, and you’re about to write a useEffect or a state toggle just to change a parent’s background color because a specific child element exists? Or maybe you’re nesting SCSS six levels deep just to target three different headings? We’ve all been there. But here is the thing: CSS has grown up. A lot of the logic we used to offload to JavaScript or complex class naming conventions can now be handled natively, elegantly, and with way less code.

Today, we are dusting off some advanced selectors that have moved from "experimental" to "production-ready." If you aren't using these yet, you're essentially working with one hand tied behind your back.

How We Suffered Before (The Dark Ages of DOM Manipulation)

Remember the struggle of styling a card differently if it contained an image? We had to use JavaScript to check for the element's presence and then toggle a class like .card--has-image. It felt dirty, it caused layout shifts, and it added unnecessary weight to our bundles.

And don’t even get me started on specificity wars. We used to write massive selector chains like .sidebar .widget .title h2 just to override a single margin. This led to a maintenance nightmare where changing one class name broke half the site. If you’ve ever fought with the cascade, you know exactly why understanding how to properly work with cascade specificity is the difference between a clean codebase and a "CSS-in-JS" escape room.

The Modern Way in 2026: The Power Trio

The game has changed. We now have the "Parent Selector" we’ve been dreaming of for twenty years, along with tools to manage grouping and specificity without breaking a sweat.

  • :has() – The undisputed king. It allows you to style an element based on what’s happening inside it. Want to style a form fieldset only if it contains an invalid input? fieldset:has(input:invalid) does it instantly.
  • :is() – Stop repeating yourself. Instead of writing header h1, header h2, header p, you can just write header :is(h1, h2, p). It takes the specificity of the most specific element in the list.
  • :where() – Identical to :is(), but with a superpower: it has zero specificity. This is perfect for reset stylesheets or base components where you want the styles to be easily overridable.

For example, if you are working on a complex UI like customization of interactive details and summary tags, using :has() can let you style the <details> wrapper only when it’s in an "open" state or contains a specific highlight class, without adding extra JS listeners.

Ready-to-Use Code Snippet

Here is a practical example of how these selectors clean up a standard UI card component. Notice how we handle state and grouping without a single line of JavaScript.

/* 1. The Parent Selector: Style the card only if it contains a featured image */
.card:has(img.featured) {
  grid-column: span 2;
  border: 2px solid gold;
}

/* 2. Grouping with :is(): Cleanly target multiple sub-elements */
.card :is(h2, h3, .title) {
  margin-top: 0;
  color: #222;
  line-height: 1.2;
}

/* 3. Logic-based styling: Darken background if a checkbox inside is checked */
.card:has(input[type="checkbox"]:checked) {
  background-color: #f0f0f0;
  opacity: 0.8;
}

/* 4. Using :where() for low-priority base styles */
:where(.card p) {
  color: #666;
  font-size: 0.9rem;
}

Common Beginner Mistake

The biggest trap devs fall into is forgetting the specificity difference between :is() and :where(). Since :is() adopts the specificity of its most "powerful" argument, you might accidentally create a selector that is nearly impossible to override later. I’ve seen devs use :is(#hero, p) and then wonder why they can’t change the paragraph color with a simple class. If you are building a library or a shared component, default to :where(). It keeps your CSS "polite" by allowing future developers to override styles without resorting to !important.

Also, while :has() is incredibly powerful, avoid using it at the top level of massive DOM trees (like body:has(...)) for every little animation. While browsers are fast, overusing complex relational selectors in heavy loops can still impact rendering performance. Use it where it makes sense: at the component level.

🔥 We publish more advanced CSS tricks, ready-to-use snippets, and tutorials in our Telegram channel. Subscribe so you don't miss out!

Top comments (0)