DEV Community

Akash for MechCloud Academy

Posted on

Exploring the :has() Pseudo-class in CSS

The :has() pseudo-class, introduced as part of the CSS Selectors Level 4 specification, is a game-changer for web developers. Often described as the "parent selector" CSS never had, :has() allows you to style elements based on the presence of specific descendants, unlocking new possibilities for dynamic and precise styling. This blog post dives into what :has() is, how it works, its practical use cases, and potential pitfalls to watch out for.

What Is the :has() Pseudo-class?

The :has() pseudo-class is a relational selector that targets an element if it contains at least one descendant matching the specified selector. Unlike traditional pseudo-classes like :hover or :first-child, which focus on an element’s state or position, :has() looks at the element’s contents. Its syntax is straightforward:

selector:has(selector) {
  /* styles */
}
Enter fullscreen mode Exit fullscreen mode

For example, to style a <div> that contains a <p> element:

div:has(p) {
  border: 2px solid blue;
}
Enter fullscreen mode Exit fullscreen mode

This applies a blue border to any <div> with at least one <p> as a descendant, regardless of nesting level. The power of :has() lies in its ability to select parent elements based on their children, something CSS lacked for years.

Browser Support and Adoption

As of June 2025, :has() enjoys robust support across modern browsers, including Chrome, Firefox, Safari, and Edge (version 105 and later). It’s considered production-ready for most projects, but always check compatibility (e.g., via CanIUse) for older browsers or specific environments. For unsupported browsers, you can use fallbacks or progressive enhancement to ensure graceful degradation.

How :has() Works

The :has() pseudo-class takes a selector as its argument and evaluates whether the element has at least one descendant matching that selector. It’s not limited to immediate children; it works with any descendant, much like the querySelectorAll method in JavaScript. Here’s a breakdown of its mechanics:

  • Selector Argument: The argument inside :has() can be any valid CSS selector, including elements (p), classes (.active), IDs (#header), or even complex combinations (a[href^="https"]).
  • Parent Targeting: It styles the parent element, not the descendant matched by the selector.
  • Dynamic Evaluation: Like other pseudo-classes, :has() updates styles dynamically as the DOM changes (e.g., via JavaScript or user interactions).

For example:

<section>
  <h2>Title</h2>
  <p>Content</p>
</section>
<section>
  <h2>Another Title</h2>
</section>
Enter fullscreen mode Exit fullscreen mode
section:has(p) {
  background: lightblue;
}
Enter fullscreen mode Exit fullscreen mode

Only the first <section> gets a light blue background because it contains a <p>.

Practical Use Cases

The :has() pseudo-class opens up a wide range of styling possibilities that previously required JavaScript or convoluted CSS workarounds. Here are some practical applications:

1. Styling Containers Based on Content

You can style a container differently depending on its children. For instance, highlight articles with images:

article:has(img) {
  border-left: 4px solid green;
}
Enter fullscreen mode Exit fullscreen mode

This adds a green border to any <article> containing an <img>, making it visually distinct.

2. Form Validation Feedback

Enhance form UX by styling fields based on their state:

.form-group:has(input:invalid) {
  border: 1px solid red;
}
.form-group:has(input:valid) {
  border: 1px solid green;
}
Enter fullscreen mode Exit fullscreen mode

This styles the parent .form-group based on whether its <input> is valid, providing clear visual cues without JavaScript.

3. Conditional Component Styling

In component-based frameworks, :has() can style wrappers based on child states. For example, style a card differently if it contains an active button:

.card:has(.btn.active) {
  box-shadow: 0 0 10px rgba(0, 0, 255, 0.3);
}
Enter fullscreen mode Exit fullscreen mode

4. Adjacent Sibling Selection

While not a direct replacement for the + or ~ combinators, :has() can emulate complex sibling relationships. For example, style a heading if the next element is a specific type:

h2:has(+ p) {
  margin-bottom: 0.5em;
}
Enter fullscreen mode Exit fullscreen mode

This reduces the margin for an <h2> followed immediately by a <p>.

5. Accessibility Enhancements

Use :has() to improve accessibility by highlighting sections with specific content, like warnings:

.section:has(.alert) {
  outline: 2px dashed orange;
}
Enter fullscreen mode Exit fullscreen mode

This draws attention to sections containing alerts, aiding users who rely on visual cues.

Combining :has() with Other Selectors

The true power of :has() emerges when combined with other CSS selectors. Here are some advanced examples:

Nested :has()

You can nest :has() for granular control:

article:has(> header:has(.featured)) {
  background: #f0f0f0;
}
Enter fullscreen mode Exit fullscreen mode

This targets an <article> with a direct <header> child that contains a .featured element.

Negation with :not()

Use :has() with :not() to style elements lacking specific descendants:

div:not(:has(p)) {
  color: gray;
}
Enter fullscreen mode Exit fullscreen mode

This grays out <div> elements without a <p>.

Pseudo-classes and States

Combine :has() with state-based pseudo-classes like :hover or :focus-within:

nav:has(a:hover) {
  background: #eee;
}
Enter fullscreen mode Exit fullscreen mode

This highlights the <nav> when any of its <a> elements are hovered.

Performance Considerations

While :has() is powerful, it’s not a free lunch. Since it evaluates descendants, it can be computationally expensive, especially in complex DOMs or with deeply nested selectors. Here are tips to optimize performance:

  • Keep Selectors Simple: Avoid overly specific or deeply nested selectors inside :has(). For example, div:has(.btn) is faster than div:has(section > div > .btn).
  • Limit Scope: Apply :has() to specific containers rather than universal selectors (*:has(...)).
  • Test Large Pages: On pages with thousands of elements, profile rendering performance using browser dev tools to catch slowdowns.

In most cases, modern browsers handle :has() efficiently, but always test in your target environment.

Gotchas and Limitations

Despite its versatility, :has() has quirks and limitations to keep in mind:

1. No Self-Referencing

You can’t use :has() to select an element based on itself (e.g., a:has(a) won’t work as expected). It’s designed for descendants, not the element itself.

2. Specificity Impact

The selector inside :has() contributes to the overall specificity of the rule. For example, div:has(#unique) has higher specificity than div:has(p) due to the ID selector. Be mindful when overriding styles.

3. Dynamic DOM Challenges

If JavaScript adds or removes elements, :has() updates styles automatically, but frequent DOM changes can cause reflows. Minimize unnecessary mutations for smooth performance.

4. Not a Full Parent Selector

While :has() is often called a parent selector, it only styles the matched element, not its children directly. To style children, you’ll need additional selectors:

div:has(.error) p {
  color: red;
}
Enter fullscreen mode Exit fullscreen mode

5. Browser Fallbacks

For unsupported browsers, provide fallbacks using feature queries or alternative styles:

/* Fallback */
div {
  border: 1px solid gray;
}

/* Modern browsers */
@supports selector(:has(*)) {
  div:has(p) {
    border: 1px solid blue;
  }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

To make the most of :has(), follow these guidelines:

  1. Use Sparingly: Reserve :has() for cases where simpler selectors (e.g., >, +, or ~) won’t suffice.
  2. Combine with Modern CSS: Pair :has() with custom properties, container queries, or other CSS advancements for maintainable code.
  3. Test Accessibility: Ensure styles applied with :has() don’t harm accessibility, such as by obscuring content or reducing contrast.
  4. Document Complex Selectors: Since :has() can create intricate rules, add comments to explain intent for future maintainers.
  5. Monitor Performance: On large-scale projects, benchmark :has() usage to avoid rendering bottlenecks.

Real-World Example

Imagine a blog layout where articles with embedded videos should stand out:

<article>
  <h2>Post 1</h2>
  <p>Text content</p>
</article>
<article>
  <h2>Post 2</h2>
  <video src="example.mp4"></video>
</article>
Enter fullscreen mode Exit fullscreen mode
article:has(video) {
  padding: 1em;
  background: linear-gradient(to right, #f0f4ff, #ffffff);
  border-radius: 8px;
}
Enter fullscreen mode Exit fullscreen mode

This styles only the second <article> with a video, creating a visually distinct presentation without JavaScript.

Conclusion

The :has() pseudo-class is a transformative addition to CSS, enabling developers to write more intuitive and flexible styles. By targeting elements based on their descendants, it fills a long-standing gap in CSS’s capabilities, reducing reliance on JavaScript for dynamic styling. From form validation to conditional layouts, its use cases are vast, but it demands careful use to avoid performance pitfalls or specificity wars. As browser support solidifies, :has() is poised to become a staple in modern web development. Experiment with it in your next project, and you’ll likely find it an indispensable tool for crafting elegant, maintainable styles.

Top comments (0)