The CSS :has() pseudo-class represents one of the most significant additions to CSS in recent years. After years of developers longing for a "parent selector," :has() delivers that capability and much more. This revolutionary selector enables you to select elements based on their children or siblings--a capability that was previously impossible in pure CSS. This comprehensive guide explores the various ways to leverage :has() in modern web development, with practical code examples you can immediately apply to your projects built with frameworks like Next.js or React.
Before :has(), CSS selectors always worked in a top-down manner. You could select a child based on its parent, but not the reverse. This limitation meant developers often needed JavaScript or complex HTML restructuring to achieve certain styling effects. The :has() selector fundamentally changes this by enabling "reverse" selections that open entirely new possibilities for responsive and conditional styling.
Understanding how :has() works is essential for any modern web developer. Combined with knowledge of CSS specificity, these advanced selectors give you precise control over your stylesheets without relying on JavaScript for styling logic.
CSS :has() at a Glance
2023
Baseline Support Year
4+
Major Browsers
5+
Key Use Cases
0
JavaScript Required
What is CSS :has()?
The :has() pseudo-class is a functional CSS selector that represents an element if any of the relative selectors passed as an argument match at least one element when anchored against that element. In simpler terms, :has() allows you to select elements based on their children or siblings--a capability that was previously impossible with pure CSS MDN Web Docs' definition.
Before :has(), CSS selectors always worked in a top-down manner. You could select a child based on its parent, but not the reverse. This limitation meant developers often needed JavaScript or complex HTML restructuring to achieve certain styling effects. The :has() selector fundamentally changes this by enabling "reverse" selections.
Browser Support and Compatibility
The :has() selector achieved Baseline status in 2023, meaning it's now supported across all modern browsers including Chrome, Edge, Firefox, and Safari MDN Web Docs' browser support data. This broad support makes :has() a practical choice for production websites, though you should still consider fallback strategies for older browser versions if your audience requires it.
Key browser support milestones:
- Chrome 105+ (September 2022)
- Safari 15.4+ (March 2022)
- Firefox 121+ (December 2023)
- Edge 105+ (September 2022)
Syntax and Specificity
The :has() selector accepts a relative selector list as its argument. The specificity of :has() is determined by the most specific selector within its arguments, similar to :is() and :not() MDN Web Docs' specificity documentation. For more on how specificity works with modern CSS selectors, see our guide on CSS specificity.
/* Basic syntax */
parent:has(selector) { /* styles */ }
/* With combinators */
element:has(> child) { /* direct child */ }
element:has(+ sibling) { /* adjacent sibling */ }
element:has(~ sibling) { /* any following sibling */ }
1/* Basic syntax */2parent:has(selector) { /* styles */ }3 4/* With combinators */5element:has(> child) { /* direct child */ }6element:has(+ sibling) { /* adjacent sibling */ }7element:has(~ sibling) { /* any following sibling */ }1. Parent Selector Pattern
The most celebrated use case for :has() is the ability to style a parent element based on its children Bejamas' parent selector examples. This "parent selector" pattern opens up entirely new possibilities for responsive and conditional styling. When building custom components for your web applications, this capability significantly reduces the need for JavaScript event handlers.
Styling Cards Based on Content
Consider a card component where you want different styling when the card contains an image versus when it doesn't. With traditional CSS, you would need to add a class to the parent card manually. With :has(), the browser handles this automatically based on the card's content:
/* Add spacing when card has an image */
.card:has(img) {
padding: 1rem;
}
/* Change layout when card has both image and button */
.card:has(img) .card-content {
margin-top: 1rem;
}
/* Special styling for cards with featured class */
.card:has(.featured) {
border: 2px solid #007ab8;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
This pattern is particularly useful when working with dynamic content from APIs or content management systems where you don't control the exact HTML structure.
If you're interested in optimizing your CSS delivery along with these advanced selector techniques, check out our guide on loading only the CSS you need.
1/* Add spacing when card has an image */2.card:has(img) {3 padding: 1rem;4}5 6/* Change layout when card has both image and button */7.card:has(img) .card-content {8 margin-top: 1rem;9}10 11/* Special styling for cards with featured class */12.card:has(.featured) {13 border: 2px solid #007ab8;14 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);15}Navigation Menu Indicators
A common pattern is adding indicators to navigation items that have dropdown menus. The :has() selector makes this straightforward by detecting which nav items contain nested unordered lists:
/* Add arrow to nav items with submenus */
nav li:has(ul) > a::after {
content: "▼";
margin-left: 0.5rem;
font-size: 0.625rem;
}
/* Highlight parent items of active pages */
nav li:has(.active) > a {
font-weight: bold;
color: #007ab8;
}
This technique is especially valuable for responsive navigation menus where menu structure varies by viewport size.
1/* Add arrow to nav items with submenus */2nav li:has(ul) > a::after {3 content: "▼";4 margin-left: 0.5rem;5 font-size: 0.625rem;6}7 8/* Highlight parent items of active pages */9nav li:has(.active) > a {10 font-weight: bold;11 color: #007ab8;12}2. Previous Sibling Selection
The :has() selector also enables selecting elements based on their following siblings--a capability that was previously impossible in CSS Go Make Things' sibling selector patterns. This is particularly useful for creating separators, spacing, and conditional styling without adding classes to every element except the last one.
Creating Smart Separators
A classic use case is adding separators between items but not after the last one. Traditional approaches required JavaScript or complex :not() selectors. With :has(), this becomes elegantly simple:
/* Add border to items that have a following sibling */
.list-item:has(+ .list-item) {
border-bottom: 1px solid #e5e5e5;
}
/* For breadcrumb separators */
.breadcrumb-item:has(+ .breadcrumb-item)::after {
content: "/";
margin: 0 0.5rem;
color: #757575;
}
This pattern works perfectly for list components, breadcrumb navigation, tag clouds, and any scenario where you need visual separation between consecutive items.
1/* Add border to items that have a following sibling */2.list-item:has(+ .list-item) {3 border-bottom: 1px solid #e5e5e5;4}5 6/* For breadcrumb separators */7.breadcrumb-item:has(+ .breadcrumb-item)::after {8 content: "/";9 margin: 0 0.5rem;10 color: #757575;11}3. Form Validation and State-Based Styling
Combining :has() with form pseudo-classes creates powerful validation styling patterns Bejamas' form validation examples. This approach reduces the need for JavaScript-based form validation and enables CSS-only progressive enhancement for better user experiences.
Conditional Form Submission
Disable or enable submit buttons based on form validity. This pattern provides immediate visual feedback to users without requiring any JavaScript:
/* Style submit button when form is invalid */
form:has(input:invalid:not(:placeholder-shown)) button[type="submit"] {
opacity: 0.5;
cursor: not-allowed;
}
form:has(input:valid) button[type="submit"] {
background-color: #2cad4e;
}
Real-Time Validation Feedback
Provide immediate visual feedback as users complete form fields. The floating label pattern becomes much simpler with :has():
/* Highlight label when input has content */
.input-group:has(input:not(:placeholder-shown)) label {
transform: translateY(-1rem);
font-size: 0.75rem;
color: #007ab8;
}
/* Add success indicator when field is valid */
.field-wrapper:has(input:valid)::after {
content: "✓";
color: #2cad4e;
position: absolute;
right: 0.5rem;
top: 50%;
transform: translateY(-50%);
}
1/* Style submit button when form is invalid */2form:has(input:invalid:not(:placeholder-shown)) button[type="submit"] {3 opacity: 0.5;4 cursor: not-allowed;5}6 7form:has(input:valid) button[type="submit"] {8 background-color: #2cad4e;9}10 11/* Highlight label when input has content */12.input-group:has(input:not(:placeholder-shown)) label {13 transform: translateY(-1rem);14 font-size: 0.75rem;15 color: #007ab8;16}4. Quantity Queries with :has()
The :has() selector enables "quantity queries"--styling elements based on the number of their children Bejamas' quantity query examples. This is invaluable for responsive card grids and list layouts where you want different layouts based on content volume.
Styling Based on Child Count
Creating different layouts for grids with varying numbers of items becomes remarkably simple with :has():
/* Cards with exactly 3 children get special styling */
.grid-item:has(> :nth-child(3):last-child) {
flex-basis: calc(33.333% - 1rem);
}
/* Items with many children get multi-line styling */
.tag-container:has(> :nth-child(5)) {
flex-wrap: wrap;
}
/* Style parent when it has at least 3 items */
.product-list:has(> .product:nth-child(n+3)) {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
Complex Quantity Queries
For more sophisticated layouts that need to adapt to specific ranges of child counts:
/* At most 3 children (3 or less, excluding 0) */
ul:has(> :nth-child(n+3):last-child) {
outline: 1px solid red;
}
/* More than 3 children */
ul:has(> :nth-child(4)) {
padding-bottom: 1rem;
}
/* Between 2 and 4 children */
ul:has(> :nth-child(2)):has(not(> :nth-child(5))) {
justify-content: center;
}
If you're exploring modern CSS tools for these kinds of layouts, you might also be interested in learning about UnoCSS as an alternative to Tailwind.
1/* Cards with exactly 3 children get special styling */2.grid-item:has(> :nth-child(3):last-child) {3 flex-basis: calc(33.333% - 1rem);4}5 6/* Items with many children get multi-line styling */7.tag-container:has(> :nth-child(5)) {8 flex-wrap: wrap;9}10 11/* Style parent when it has at least 3 items */12.product-list:has(> .product:nth-child(n+3)) {13 display: grid;14 grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));15}5. Combining :has() with :not()
The real power of :has() emerges when combined with other pseudo-classes like :not() Bejamas' :has() + :not() combinations. This combination enables "negative" selection patterns--styling based on what elements are missing rather than what they contain.
Styling Based on Missing Content
This is invaluable for accessibility debugging and creating fallback layouts:
/* Cards without images get different layout */
.card:not(:has(img)) {
flex-direction: column;
}
/* Images without alt attributes need attention */
article:has(img:not([alt])) {
border: 2px dashed #f7272f;
}
/* Forms without required fields need no special styling */
.form-section:not(:has([required])) {
background-color: transparent;
padding: 0;
}
Complex Conditional Styling
For more sophisticated accessibility and layout patterns:
/* Elements with images that are missing alt text */
.post:has(img:not([alt])) {
outline: 2px dashed #f7272f;
}
/* Cards with images but no buttons */
.card:has(img):not(:has(.btn)) {
padding-bottom: 2rem;
}
The combination of :has() with :not() is particularly powerful for accessibility auditing where you need to identify missing attributes across your site.
1/* Cards without images get different layout */2.card:not(:has(img)) {3 flex-direction: column;4}5 6/* Images without alt attributes need attention */7article:has(img:not([alt])) {8 border: 2px dashed #f7272f;9}10 11/* Forms without required fields need no special styling */12.form-section:not(:has([required])) {13 background-color: transparent;14 padding: 0;15}Performance Considerations
When using :has(), consider these performance implications MDN Web Docs' performance notes:
-
Selector Complexity: More complex selectors within :has() have higher performance costs. The browser must evaluate the condition for each element, so deeply nested or compound selectors will impact rendering performance more than simple selectors.
-
Avoid Nesting: The :has() selector cannot be nested within another :has(). Attempting to do so will result in invalid CSS that browsers will ignore entirely.
-
Pseudo-elements: Pseudo-elements are not valid within :has() selectors, and pseudo-elements cannot be used as anchors for :has(). This is to prevent circular querying that could create infinite loops.
-
Browser Engine: Modern browsers have optimized :has() well, with dedicated code paths in their rendering engines. However, test on target devices, especially lower-powered mobile devices.
Best Practices
-
Keep selectors simple within :has() arguments. Prefer class selectors over complex attribute selectors when possible.
-
Use :has() for progressive enhancement rather than critical layout. For essential functionality, consider fallback JavaScript implementations.
-
Test performance on lower-powered devices and measure with browser dev tools. Chrome's Performance tab can show you exactly how long :has() selectors take.
-
Consider CSS containment for better performance. Using
contain: layoutorcontain: painton containers with :has() selectors can improve rendering performance by limiting the scope of layout calculations. -
Batch updates where possible. When multiple elements use :has() selectors that affect similar properties, consider consolidating them to reduce reflows.
For complex applications, especially those built with React or Next.js, these performance considerations become even more important as component re-renders can trigger selector re-evaluation.
| Use Case | Example Pattern |
|---|---|
| Parent styling | .card:has(img) |
| Sibling detection | .item:has(+ .item) |
| Form validation | form:has(input:invalid) |
| Quantity queries | .list:has(> :nth-child(4)) |
| Content filtering | .card:not(:has(img)) |
| Navigation indicators | nav li:has(ul) > a::after |
Conclusion
The CSS :has() pseudo-class revolutionizes what you can accomplish with pure CSS. From parent selectors to quantity queries, :has() enables patterns that previously required JavaScript or complex HTML restructuring. With universal browser support since late 2023, :has() is now ready for production use in modern web applications.
Start incorporating :has() into your projects today to reduce JavaScript dependencies, improve maintainability, and create more adaptive, responsive interfaces. Whether you're building e-commerce platforms, SaaS applications, or corporate websites, :has() provides a powerful tool for creating sophisticated, responsive layouts with clean, maintainable CSS.
The key is to use :has() strategically--focus on progressive enhancement where it improves the experience without breaking functionality for older browsers. With the practical examples in this guide, you have everything you need to start leveraging :has() in your own projects. To further optimize your CSS workflow, consider exploring modern approaches to loading only the CSS you need.
Frequently Asked Questions
Sources
- MDN Web Docs - CSS :has() - Official CSS specification reference with browser support data
- Bejamas - Learn CSS :has() Selector by Examples - Practical code examples and demos
- Go Make Things - The :has() CSS Pseudo-Class - Developer-focused practical examples