The Magic of CSS :has() Pseudo-class
Introduction
For decades, CSS developers have made the same plea: “We need a parent selector.” If a child element existed in a specific state, there was no native way in CSS to style its parent container. We relied heavily on JavaScript to toggle classes on parent elements based on DOM events.
With the widespread adoption of the :has() pseudo-class, that era is over. Often referred to as the “parent selector,” :has() is actually much more powerful than that. It is a relational pseudo-class that allows you to select an element based on its descendants, its siblings, or any complex combination of the two. In this guide, we’ll explore why :has() is a game-changer for CSS architecture.
Core Concepts: How :has() Works
The syntax is straightforward: element:has(selector). If the condition inside the parenthesis is true, the element is selected.
For example, article:has(img) will select any <article> tag that contains an <img> anywhere within it. This allows you to completely alter the layout of the article (e.g., applying a CSS Grid layout) only when an image is present, without needing a special .article-with-image class generated by your backend.
Beyond the Parent: Relational Styling
Because :has() can evaluate complex selectors, it unlocks patterns that previously required extensive scripting.
Previous Sibling Selection
CSS has always had the adjacent sibling combinator (+) and the general sibling combinator (~), but they only work forward. You could style an element based on the element before it, but not the other way around. With :has(), you can!
/* Style a label ONLY if the input immediately following it is checked */
label:has(+ input[type="checkbox"]:checked) {
font-weight: bold;
color: green;
}
Code Example: Interactive UI without JS
Let’s build a highly requested UI pattern: a navigation menu that dims all other links when you hover over one specific link.
<ul class="nav-menu"> <li><a href="#">Home</a></li> <li><a href="#">About</a></li> <li><a href="#">Services</a></li> <li><a href="#">Contact</a></li> </ul>
/* Base transition for smooth effects */
.nav-menu a {
transition: opacity 0.3s ease, transform 0.3s ease;
color: black;
text-decoration: none;
font-size: 1.2rem;
}
/*
THE MAGIC:
If the .nav-menu HAS any link that is currently being hovered,
target ALL links inside the menu and fade them out.
*/
.nav-menu:has(a:hover) a {
opacity: 0.3;
}
/*
Override the fade for the specifically hovered link
*/
.nav-menu a:hover {
opacity: 1;
transform: scale(1.1);
}
This entire interaction is achieved with three lines of logic in CSS. No JavaScript event listeners, no state management.
Common Mistakes / Pitfalls
- Performance Concerns with Deep Nesting: Browsers evaluate
:has()incredibly fast, but deeply nested and overly complex:has()selectors (e.g., checking deeply nested trees with general siblings) can theoretically cause rendering bottlenecks on lower-end devices if applied to thousands of elements. Keep conditions concise. - Misunderstanding Specificity: The specificity of the
:has()pseudo-class is determined by the most specific selector within its parentheses.article:has(#featured)has a much higher specificity thanarticle:has(.image). Be careful that a high-specificity condition doesn’t accidentally override your other styles. - Using it as a Crutch for Bad HTML: While
:has()can fix styling issues caused by poor DOM structure, you should still strive to write semantic HTML. Don’t use:has()to avoid putting classes where they logically belong.
Best Practices
- Use for Form Validation UI:
:has()is exceptional for forms. You can style the entire form container (like adding a red border to a fieldset) if:has(:invalid)is true, providing instant visual feedback without JavaScript. - Combine with Container Queries:
:has()pairs beautifully with CSS Container Queries. You can create components that adapt their internal layout not just based on their width, but based on the specific content they contain. - Check Browser Support: While baseline support is now excellent across all major modern browsers, if you support legacy enterprise environments (like older versions of Safari), ensure you provide sensible fallbacks.
Conclusion
The :has() pseudo-class is not just a “parent selector”—it’s a conditional logic engine built directly into CSS. By shifting state-based styling away from JavaScript and into the stylesheet, we can write cleaner, more declarative, and more maintainable frontend code. It is undoubtedly one of the most powerful features added to CSS in recent history.
FAQ
Is :has() supported in all browsers?
As of late 2023, :has() is fully supported in Chrome, Safari, Firefox, and Edge. It is safe for modern web development, though fallbacks may be required for older enterprise browser versions.
Can I nest :has() inside another :has()?
No, the CSS specification currently forbids nesting :has() pseudo-classes within each other to prevent recursive performance issues in the rendering engine.
Does :has() work with pseudo-elements like ::before?
No. You cannot use pseudo-elements inside the :has() condition because pseudo-elements do not exist in the DOM tree in a way that can be queried relationally. You can only query real DOM elements and state pseudo-classes.
editor's pick
latest video
news via inbox
Nulla turp dis cursus. Integer liberos euismod pretium faucibua

