Advanced Guide to the CSS :has() Selector

Master parent selection, conditional styling, and layout detection with CSS's most powerful selector. Learn practical patterns for modern web development.

What is the CSS :has() Selector?

The CSS :has() pseudo-class represents one of the most significant additions to CSS in recent years. Often called the "parent selector," :has() allows you to select elements based on their children or preceding siblings--something that was previously impossible with CSS alone. This capability opens up entirely new possibilities for conditional styling, reducing the need for JavaScript for many common UI patterns.

Key capabilities:

  • Select parent elements based on their children
  • Style elements based on preceding siblings
  • Create conditional styling without JavaScript
  • Detect layout states and element relationships

Since achieving Baseline status in December 2023, :has() is now supported across all modern browsers, making it safe to use in production applications.

The :has() pseudo-class is a functional selector that takes a relative selector list as its argument. When applied to an element, it matches if any of the selectors within the :has() argument match at least one element relative to the base element. In simpler terms, :has() lets you style an element based on what it contains or what comes before it.

Prior to :has(), CSS could only select forward--down the DOM tree or to subsequent siblings. You could select a child based on its parent, but not vice versa. The :has() selector fundamentally changes this limitation, enabling what developers have wanted for decades: a true parent selector.

Browser Support Status

4+

Major Browsers

Dec 2023

Baseline Status

100%

Modern Coverage

Basic Syntax and Selector Patterns

Syntax Structure

The :has() pseudo-class accepts a relative selector list as its argument:

element:has(relative-selector) {
 /* styles */
}

The relative selector can use any combinator (descendant, child, adjacent sibling, general sibling) to select elements relative to the base element. This flexibility is what makes :has() so powerful.

Child Selection Pattern

The most common use of :has() is selecting parents based on their children:

/* Select article that contains an h1 */
article:has(h1) {
 /* styles */
}

/* Select div with direct child having class 'special' */
div:has(> .special) {
 /* styles */
}

/* Select nav that contains an anchor with class 'active' */
nav:has(a.active) {
 /* styles */
}

Sibling Selection Pattern

The :has() selector also enables selecting elements based on preceding siblings, which was previously impossible:

/* Select h1 that is immediately followed by h2 */
h1:has(+ h2) {
 /* styles */
}

/* Select paragraph that has a preceding sibling paragraph */
p:has(~ p) {
 /* styles */
}

/* Select section containing an h2 followed by a paragraph */
section:has(h2 + p) {
 /* styles */
}

This sibling selection capability is particularly valuable for styling document structures where you need to apply different formatting based on what follows an element. Combined with our UI/UX design services, these patterns enable more sophisticated and maintainable styling architectures.

Parent Selection: The Killer Feature

Styling Containers Based on Content

The ability to style a parent element based on its children represents a fundamental shift in how we approach CSS architecture. Previously, developers had to add classes to parent elements manually or use JavaScript to toggle classes when child content changed. With :has(), this becomes purely declarative in CSS.

/* Highlight cards that contain a badge */
.card:has(.badge) {
 border-color: #3b82f6;
 background-color: #eff6ff;
}

/* Add spacing to form groups that have error messages */
.form-group:has(.error-message) {
 margin-bottom: 1.5rem;
}

/* Style article cards differently if they have images */
.article-card:has(img) {
 grid-row: span 2;
}

Direct vs. Descendant Selection

The :has() selector supports both direct child selectors (>) and descendant selectors (space). The distinction matters for targeting specificity:

/* Any descendant with class 'featured' */
section:has(.featured) {
 /* matches if .featured exists anywhere in section */
}

/* Direct child with class 'featured' */
section:has(> .featured) {
 /* matches only if .featured is a direct child */
}

Multiple Conditions

You can combine multiple conditions within a single :has() selector for complex matching:

/* Section that contains both a heading and an image */
section:has(h2:has(+ img)) {
 /* complex nesting for precise matching */
}

/* Card with both a thumbnail and a badge */
.card:has(.thumbnail):has(.badge) {
 /* matches elements satisfying both conditions */
}

This pattern is essential for frontend development teams building complex component systems where conditional styling based on child content is common.

Previous Sibling Selection

The Adjacent Sibling Combinator

The adjacent sibling combinator (+) with :has() allows styling an element based on what immediately follows it:

/* Reduce margin on h1 if it's followed by h2 */
h1:has(+ h2) {
 margin-bottom: 0.25rem;
}

/* Style list items that are followed by another list item */
li:has(+ li) {
 border-top: 1px solid #e5e7eb;
}

This pattern is particularly useful for typographic adjustments and visual separators in responsive layouts.

The General Sibling Combinator

The general sibling combinator (~) enables selection based on any following sibling:

/* Style figure that has a caption following it anywhere */
figure:has(~ figcaption) {
 margin-bottom: 1rem;
}

/* Highlight sections that contain a warning */
section:has(.warning ~ *) {
 /* any element after .warning */
}

Practical Sibling Patterns

Sibling selection with :has() solves many common styling challenges:

/* Highlight input when its label is required */
input:has(+ .required-label) {
 border-color: #ef4444;
}

/* Style section if it contains a blockquote */
section:has(blockquote) {
 border-left: 4px solid #6366f1;
 padding-left: 1rem;
}

These techniques integrate seamlessly with our custom web application development services, enabling more maintainable CSS architectures.

Conditional Styling Based on Element State

Form Validation States

The :has() pseudo-class works excellently with form validation, allowing you to style entire form groups based on input states:

/* Style the form group when the input is invalid */
.form-group:has(input:invalid:not(:placeholder-shown)) {
 border-color: #ef4444;
}

/* Show error message container when input is invalid */
.form-group:has(input:invalid) .error-message {
 display: block;
}

/* Style fieldset when it contains at least one checked radio */
fieldset:has(input[type="radio"]:checked) {
 background-color: #f0fdf4;
}

Focus and Hover States

You can create sophisticated interaction patterns using :has() with pseudo-classes:

/* Style parent card when any child input has focus */
.card:has(input:focus) {
 box-shadow: 0 0 0 2px #3b82f6;
}

/* Style parent when any child has hover */
.card:has(.btn:hover) {
 transform: translateY(-2px);
}

/* Show tooltip when parent has hover */
.tooltip-container:has(:hover) .tooltip {
 opacity: 1;
 visibility: visible;
}

Checkbox and Radio States

Creating interactive patterns without JavaScript:

/* Style the card when the checkbox is checked */
.card:has(input[type="checkbox"]:checked) {
 background-color: #dbeafe;
 border-color: #3b82f6;
}

/* Show content when associated checkbox is checked */
.content:has(#toggle:checked) {
 display: block;
}

/* Style the label when its checkbox is checked */
label:has(input:checked) {
 color: #16a34a;
 font-weight: 600;
}

These CSS-only patterns reduce JavaScript dependencies and improve performance for progressive web applications.

Layout Detection Patterns

Empty State Detection

Detecting and styling empty containers is a common requirement in web applications:

/* Style empty containers differently */
.card:empty {
 min-height: 200px;
 display: flex;
 align-items: center;
 justify-content: center;
 background-color: #f3f4f6;
}

/* Add placeholder to empty sections */
.section:empty::before {
 content: "No content available";
 color: #9ca3af;
}

Overflow Detection

You can detect when content overflows its container:

/* Style container when it has overflowing content */
.container:has(.content:overflow) {
 overflow-x: auto;
}

/* Add indicator when content is scrollable */
.list-container:has(.list-item:nth-child(20)) {
 /* indicate scrollable when at least 20 items */
}

Scroll Position Detection

More advanced scroll-related styling:

/* Style page header when scrolled past a certain point */
body:has(header[data-scrolled="true"]) .scroll-indicator {
 opacity: 0;
}

/* Highlight active section in navigation based on scroll */
.section:has(id):hover ~ nav .nav-link[href="#${id}"] {
 /* link activation based on scroll */
}

These layout detection patterns are particularly valuable for single page application development where sophisticated UI interactions are expected.

Advanced Selector Combinations

Combining with :is()

The :is() pseudo-class works seamlessly with :has() for grouping selectors:

/* Select any heading followed by a paragraph */
:is(h1, h2, h3):has(+ p) {
 margin-bottom: 0.5rem;
}

/* Style card if it contains any of these elements */
.card:has(:is(img, video, iframe)) {
 aspect-ratio: 16 / 9;
}

Combining with :not()

The :not() pseudo-class enables negative conditions for more precise selection:

/* Select articles that don't have a featured class */
article:has(p):not(:has(.featured)) {
 /* styles for articles with paragraphs but not featured */
}

/* Style containers that have images but not thumbnails */
.container:has(img):not(:has(.thumbnail)) {
 /* specific styling */
}

Multiple :has() Conditions

You can chain multiple :has() selectors for complex conditions:

/* Element that contains both an image and a heading */
.card:has(img):has(h2) {
 /* matches if both conditions are true */
}

/* Section that has a heading followed by a specific element */
section:has(h2:has(+ .special)) {
 /* nested conditions */
}

These advanced combinations are fundamental to building maintainable CSS architectures for enterprise web solutions.

Performance Considerations

Browser Engine Implementation

Modern browsers have optimized :has() selection significantly. The selector is evaluated using efficient tree traversal algorithms, but there are still considerations for complex selectors:

  • Simple selectors are faster: element:has(.class) is faster than element:has(.class1 .class2 .class3)
  • Specificity inheritance: :has() takes the specificity of its most specific selector argument
  • Avoid deep nesting: Very deep :has() chains can impact performance

Best Practices for Performance

/* Good: Simple, specific selectors */
.card:has(.featured) { /* fast */ }

/* Avoid: Overly complex nested selectors */
.card:has(.content .item .special .highlight) { /* potentially slow */ }

/* Consider: Breaking complex selectors into multiple rules */
.card:has(.highlight) {
 /* if additional conditions needed, use separate rules */
}

When to Use :has() vs. JavaScript

Use :has() when:

  • Styling based on child/sibling relationships
  • Creating CSS-only interactive patterns
  • Reducing JavaScript for conditional styling
  • Building component libraries with conditional styling

Consider JavaScript when:

  • Complex DOM manipulation is needed
  • Cross-browser support for very old browsers is required
  • State logic is complex beyond CSS capabilities
  • Animation sequences require precise timing control

Our performance optimization services can help ensure your CSS architecture remains performant as your application grows.

Practical Real-World Examples

Card-Based UI Patterns

/* Different card styles based on content */
.product-card:has(.sale-badge) {
 border-color: #ef4444;
}

.product-card:has(.review-score) {
 padding-right: 1rem;
}

/* Premium card styling */
.premium-card:has(.exclusive-feature) {
 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

Navigation Enhancements

/* Highlight nav items with dropdowns */
.nav-item:has(.dropdown) > a::after {
 content: " ▼";
}

/* Active state based on section presence */
.nav-link:has(~ .section.active) {
 color: #3b82f6;
}

Responsive Patterns

/* Change grid columns based on content presence */
.grid-item:has(img) {
 grid-column: span 2;
}

@media (max-width: 768px) {
 .grid-item:has(img) {
 grid-column: span 1;
 }
}

Common Pitfalls and How to Avoid Them

Understanding What :has() Selects

A common misconception is that :has() selects the element inside the parentheses. In reality, :has() selects the element to which it is attached based on what its argument matches elsewhere in the DOM.

Pseudo-element Limitations

Pseudo-elements cannot be used as selectors within :has() or as anchors for :has(). This prevents circular queries that could cause performance issues.

Nesting Restrictions

The :has() pseudo-class cannot be nested within another :has(). This limitation exists to prevent complex circular dependency scenarios.

Fallback Considerations

For critical functionality, consider providing fallback styles:

/* Graceful degradation */
.card {
 /* base styles for all browsers */
}

.card:has(.badge) {
 /* enhanced styles for :has() support */
}

/* JavaScript fallback for older browsers */
.no-supports-has .card.badge {
 /* alternative styles */
}

Conclusion

The CSS :has() pseudo-class represents a transformative addition to the CSS selector toolkit. By enabling parent and previous sibling selection, it eliminates the need for many JavaScript workarounds and allows for more declarative, maintainable CSS.

With Baseline browser support since December 2023, :has() is ready for production use in modern web applications. The key to effectively using :has() lies in understanding its patterns: parent selection based on children, conditional styling based on presence or state, and layout detection.

By mastering these patterns, you can write more efficient CSS while reducing reliance on JavaScript for conditional styling logic. As browser engines continue to optimize :has() performance and adoption grows, we can expect to see increasingly sophisticated CSS-only solutions for UI patterns that previously required JavaScript.

The :has() selector is not just a feature--it's a new way of thinking about CSS architecture. Whether you're building responsive website designs or complex web application interfaces, :has() provides capabilities that were previously impossible without JavaScript.

Our team at Digital Thrive specializes in leveraging cutting-edge CSS capabilities like :has() to build better, faster, and more maintainable web experiences. Contact us to learn how we can help modernize your web development workflow.

Frequently Asked Questions

What browsers support :has()?

The :has() selector has full support in Chrome 105+, Edge 105+, Safari 15.4+, and Firefox 121+. It achieved Baseline status in December 2023, making it safe for production use across all modern browsers.

Can :has() be nested?

No, the :has() pseudo-class cannot be nested within another :has() selector. This restriction exists to prevent circular dependency issues that could cause performance problems.

What is the performance impact of :has()?

Modern browsers have optimized :has() significantly. Simple selectors like .card:has(.badge) are fast, but very deeply nested selectors may impact performance. Test with your specific use cases and prefer simple, specific selectors.

How is :has() different from :not()?

:has() selects elements based on what they contain or precede, while :not() excludes elements matching a selector. They can be combined for powerful negative and positive conditions, such as :not(:has(.excluded)).

Does :has() work with pseudo-elements?

No, pseudo-elements cannot be used as selectors within :has() or as anchors for :has(). This prevents circular queries that could cause performance issues and ambiguous selector matching.

When should I use :has() vs JavaScript?

Use :has() for styling based on DOM relationships, conditional styles, and CSS-only interactions. Use JavaScript for complex DOM manipulation, state logic, or when supporting very old browsers that don't support :has().

Ready to Modernize Your Web Development?

Our team specializes in cutting-edge web technologies and can help you implement advanced CSS patterns like :has() for better, faster websites.

Sources

  1. MDN Web Docs - :has() Selector Reference - Official documentation for CSS :has() pseudo-class including syntax, examples, browser compatibility, and performance considerations.

  2. LogRocket Blog - Advanced Guide to CSS :has() Selector - Practical implementation guide covering parent selectors, child element detection, form styling, layout detection, and combination patterns with :is() and :not().