For over two decades, web developers have asked for one thing: a parent selector in CSS. The ability to style an element based on its children seemed like an impossible dream. JavaScript was our only escape hatch, leading to countless workarounds, state management headaches, and performance compromises.
Then came :has().
The CSS :has() selector fundamentally changes how we think about styling web interfaces. It's not just a parent selectorâit's a relational pseudo-class that lets you select elements based on what they contain, what comes after them, or virtually any relationship you can express in CSS.
In this comprehensive guide, we'll explore everything you need to know about :has(): from basic syntax to advanced patterns, performance considerations, and real-world applications that will transform your CSS architecture.
The Long Wait for a Parent Selector
Before diving into :has(), let's understand why this feature took so long to arrive and why it matters so much.
The Historical Problem
Traditional CSS selectors work in one direction: from parent to child. You can style a child based on its parent, but never the reverse.
/* This works - styling children based on parent */
.card .title {
font-size: 1.5rem;
}
/* This was impossible - styling parent based on child */
/* .card:contains(.featured-badge) { ... } â NOT VALID CSS (before :has()) */
This limitation forced developers into awkward patterns:
-
Adding modifier classes:
.card.has-featured-badge { ... }
-
JavaScript-based styling: Listening for DOM changes and toggling classes
-
Restructuring HTML: Moving elements around to fit CSS limitations
Each approach had drawbacks. Modifier classes created coupling between logic and styles. JavaScript solutions added complexity and potential performance issues. HTML restructuring compromised semantic markup.
Why It Took So Long
Browser vendors hesitated to implement parent selectors due to performance concerns. CSS selectors are evaluated right-to-left for efficiency. A parent selector could theoretically require the browser to:
- Find all matching descendant elements
- Walk up the DOM tree for each match
- Apply styles to ancestors
This approach could cause significant rendering bottlenecks on complex pages. The :has() specification addresses these concerns with specific parsing and evaluation rules, making it performant enough for real-world use.
Understanding :has() Fundamentals
The :has() pseudo-class accepts a relative selector list as its argument and matches elements that have at least one descendant matching that selector.
Basic Syntax
parent:has(child-selector) {
/* styles applied to parent */
}
The element before :has() is styled when it contains any element matching the selector inside the parentheses.
Your First :has() Example
Let's start with a practical example. Consider a card component:
<div class="card">
<img src="product.jpg" alt="Product">
<h3>Product Title</h3>
<p>Product description...</p>
</div>
<div class="card">
<h3>Text-Only Card</h3>
<p>This card has no image.</p>
</div>
Previously, styling cards differently based on whether they contain an image required JavaScript or separate CSS classes. With :has():
/* Default card styles */
.card {
padding: 1rem;
border-radius: 8px;
background: white;
}
/* Cards WITH images get a different layout */
.card:has(img) {
display: grid;
grid-template-columns: 200px 1fr;
gap: 1rem;
}
/* Cards WITHOUT images center their content */
.card:not(:has(img)) {
text-align: center;
max-width: 400px;
}
This is pure CSS. No JavaScript. No extra classes. The styling adapts automatically based on content.
Beyond Parent Selection: :has() as a Relational Selector
While "parent selector" captures the most common use case, :has() is far more powerful. It's a relational pseudo-class that can express complex element relationships.
Sibling Selection with :has()
You can select elements based on their siblings using :has() with sibling combinators:
/* Select a label that has a required input after it */
label:has(+ input:required) {
font-weight: bold;
}
label:has(+ input:required)::after {
content: " *";
color: #e74c3c;
}
This pattern is incredibly useful for form styling without JavaScript.
Ancestor Selection
:has() can look multiple levels deep:
/* Style a section if it contains ANY error anywhere inside */
.form-section:has(.error-message) {
border-left: 4px solid #e74c3c;
background: #fdf2f2;
}
/* Style a table row if it contains an editable cell */
tr:has(td[contenteditable="true"]) {
background: #fffef0;
}
Combining Multiple Conditions
You can chain multiple :has() conditions:
/* Card with both an image AND a featured badge */
.card:has(img):has(.featured-badge) {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
border: 2px solid gold;
}
Or use a selector list within :has():
/* Card with either a video OR an image */
.card:has(img, video) {
min-height: 300px;
}
Real-World Use Cases and Patterns
Let's explore practical applications that showcase the true power of :has().
1. Form Validation Styling
One of the most impactful uses of :has() is CSS-only form validation feedback:
/* Style form group based on input validity */
.form-group:has(input:invalid:not(:placeholder-shown)) {
--input-border-color: #e74c3c;
--input-bg: #fdf2f2;
}
.form-group:has(input:valid:not(:placeholder-shown)) {
--input-border-color: #27ae60;
--input-bg: #f0fdf4;
}
.form-group input {
border: 2px solid var(--input-border-color, #ddd);
background: var(--input-bg, white);
transition: all 0.2s ease;
}
/* Show validation message only when invalid */
.form-group .error-message {
display: none;
color: #e74c3c;
font-size: 0.875rem;
margin-top: 0.5rem;
}
.form-group:has(input:invalid:not(:placeholder-shown)) .error-message {
display: block;
}
This creates a complete, reactive validation UI without a single line of JavaScript.
2. Navigation Active States
Style navigation items based on the current page or active state:
/* Highlight dropdown parent when any child link is active */
.nav-dropdown:has(.nav-link.active) > .dropdown-toggle {
color: var(--primary-color);
font-weight: bold;
}
/* Show dropdown indicator when it has submenu items */
.nav-item:has(.submenu)::after {
content: "âź";
font-size: 0.75em;
margin-left: 0.5rem;
}
3. Empty State Handling
Detect and style empty containers:
/* Style container when it has no items */
.item-grid:not(:has(.item)) {
display: flex;
align-items: center;
justify-content: center;
min-height: 300px;
}
.item-grid:not(:has(.item))::before {
content: "No items to display";
color: #999;
font-style: italic;
}
4. Quantity Queries
This is where :has() gets creative. You can style elements based on sibling count:
/* Style differently when there's only one item */
.item:only-child {
width: 100%;
}
/* When there are exactly two items */
.item:first-child:nth-last-child(2),
.item:last-child:nth-last-child(2) {
width: 50%;
}
/* Style the container when it has more than 3 items */
.item-container:has(.item:nth-child(4)) {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
5. Focus Management
Implement advanced focus states for accessibility:
/* Highlight entire card when any focusable element inside is focused */
.card:has(:focus-visible) {
outline: 3px solid var(--focus-color);
outline-offset: 2px;
}
/* Dim other cards when one is focused (for keyboard navigation) */
.card-container:has(.card:focus-visible) .card:not(:focus-visible):not(:has(:focus-visible)) {
opacity: 0.7;
}
6. Responsive Component Behavior
Create components that adapt based on their content:
/* Switch header layout based on content */
.header:has(.search-bar):has(.user-menu) {
display: grid;
grid-template-columns: auto 1fr auto;
}
.header:has(.search-bar):not(:has(.user-menu)) {
display: grid;
grid-template-columns: auto 1fr;
}
/* Adjust sidebar width when expanded sections exist */
.sidebar:has(.section.expanded) {
width: 320px;
}
.sidebar:not(:has(.section.expanded)) {
width: 240px;
}
7. Table Enhancements
Style tables dynamically based on content:
/* Highlight row when checkbox is checked */
tr:has(input[type="checkbox"]:checked) {
background: #e3f2fd;
}
/* Style header differently when table has sortable columns */
table:has(th[data-sortable]) thead {
cursor: pointer;
}
table:has(th[data-sortable]) th:hover {
background: #f5f5f5;
}
/* Add visual indicator when table is empty */
table:not(:has(tbody tr)) {
position: relative;
}
table:not(:has(tbody tr))::after {
content: "No data available";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #999;
}
Advanced Patterns and Techniques
Combining :has() with :not()
The combination of :has() and :not() unlocks powerful negation patterns:
/* Style elements that DON'T contain something */
.container:not(:has(.advertisement)) {
/* Premium, ad-free experience styles */
padding: 2rem;
}
/* Cards without media */
.card:not(:has(img, video, iframe)) {
/* Text-only card styles */
font-size: 1.1rem;
line-height: 1.8;
}
Debugging with :has()
Use :has() for development-time debugging:
/* Highlight images without alt text during development */
img:not([alt]),
img[alt=""] {
outline: 5px solid red !important;
}
/* Highlight forms with empty actions */
form:not([action]),
form[action=""] {
outline: 3px dashed orange !important;
}
/* Find links that open in new tabs without rel="noopener" */
a[target="_blank"]:not([rel*="noopener"]) {
outline: 3px solid purple !important;
}
The Forward-Looking Combinator Pattern
Use :has() with the next-sibling combinator for future element selection:
/* Style an element based on what comes AFTER it */
h2:has(+ .special-content) {
/* This h2 is followed by special content */
color: var(--accent-color);
border-bottom: 3px solid currentColor;
}
/* Create a "previous sibling" selector effect */
.item:has(+ .item.active) {
/* Style the item BEFORE the active one */
border-right: none;
}
Performance Considerations
While modern browsers handle :has() efficiently, understanding performance implications helps you write optimized CSS.
How Browsers Evaluate :has()
Browsers implement :has() with specific optimizations:
-
Invalidation limits: Browsers set limits on how deep
:has() will search
-
Subject-based caching: Once matched, results are cached for the subject element
-
Mutation batching: DOM changes are batched before re-evaluation
Best Practices for Performance
1. Be Specific with Selectors
/* â Broad selector - evaluates for all divs */
div:has(img) { ... }
/* â
Specific selector - limits scope */
.card:has(img) { ... }
2. Avoid Deep Nesting
/* â Deep nesting requires traversing entire subtree */
.page:has(.section .container .row .col .card .badge) { ... }
/* â
Direct or shallow relationships */
.card:has(> .badge) { ... }
3. Use Direct Child Combinator When Possible
/* â Searches all descendants */
.menu:has(.active) { ... }
/* â
Only checks direct children */
.menu:has(> .menu-item.active) { ... }
4. Limit Selector List Length
/* â Long selector lists in :has() */
.container:has(img, video, audio, iframe, canvas, svg, object, embed) { ... }
/* â
Semantic grouping with data attributes */
.container:has([data-media]) { ... }
Measuring Performance Impact
To measure :has() performance:
// Use the Performance API
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'style-recalc') {
console.log(`Style recalculation: ${entry.duration}ms`);
}
}
});
observer.observe({ entryTypes: ['measure'] });
Or use browser DevTools:
- Open Performance tab
- Record while interacting with elements that trigger
:has() re-evaluation
- Look for "Recalculate Style" entries
Browser Support and Fallbacks
As of late 2024, :has() enjoys excellent browser support:
-
Chrome: 105+ (August 2022)
-
Edge: 105+ (August 2022)
-
Safari: 15.4+ (March 2022)
-
Firefox: 121+ (December 2023)
-
Opera: 91+
This means over 95% of global users can use :has() today.
Creating Fallbacks
For legacy browser support, use feature queries:
/* Default fallback styles */
.card {
display: block;
}
/* Progressive enhancement with :has() */
@supports selector(:has(*)) {
.card:has(img) {
display: grid;
grid-template-columns: 200px 1fr;
}
}
The @supports Pattern
A robust pattern for :has() fallbacks:
/* Base styles - work everywhere */
.form-group {
margin-bottom: 1rem;
}
.form-group.has-error {
border-color: red;
}
/* Modern browsers with :has() support */
@supports selector(:has(*)) {
/* Remove JavaScript-dependent class usage */
.form-group.has-error {
border-color: initial;
}
/* Use :has() instead */
.form-group:has(input:invalid:not(:placeholder-shown)) {
border-color: red;
}
}
Common Mistakes and How to Avoid Them
Mistake 1: Forgetting :has() Specificity
:has() doesn't add to specificity based on its contents:
/* These have the same specificity! */
.card { ... } /* (0, 1, 0) */
.card:has(img) { ... } /* (0, 1, 0) */
The fix:
.card:has(img) {
/* Use custom properties or increasing selector specificity */
}
/* Or be explicit: */
.card.with-media:has(img) { ... } /* (0, 2, 0) */
Mistake 2: Over-Reliance on :has()
Not everything needs :has(). Simpler solutions often exist:
/* â Overcomplicating */
.btn:has(svg):has(span) { ... }
/* â
Simpler with a utility class */
.btn.icon-button { ... }
Mistake 3: Ignoring Selector Performance
/* â Expensive - checks every element */
*:has(.some-class) { ... }
/* â
Scoped appropriately */
.component:has(.some-class) { ... }
Mistake 4: Circular Dependencies
Some selectors can create infinite loops:
/* â ď¸ Be careful with self-referential patterns */
.item:has(+ .item:has(+ .item)) { ... }
The Future of CSS Selection
The :has() selector represents a fundamental shift in CSS capabilities, but it's just the beginning. Future CSS specifications are exploring:
-
CSS Nesting: Now available, works beautifully with
:has()
-
Scroll-driven animations: Combine with
:has() for complex scroll effects
-
Container queries: Pair with
:has() for truly responsive components
-
CSS Mixins: Reusable style patterns that could leverage
:has()
:has() with CSS Nesting
Modern CSS nesting makes :has() even more powerful:
.card {
padding: 1rem;
&:has(img) {
display: grid;
grid-template-columns: 200px 1fr;
& img {
border-radius: 8px;
}
}
&:has(.badge) {
position: relative;
& .badge {
position: absolute;
top: -10px;
right: -10px;
}
}
}
:has() with Container Queries
The ultimate in component-based styling:
.card-container {
container-type: inline-size;
container-name: card;
}
@container card (min-width: 400px) {
.card:has(img) {
grid-template-columns: 250px 1fr;
}
}
@container card (max-width: 399px) {
.card:has(img) {
grid-template-columns: 1fr;
}
}
Practical Refactoring: Before and After
Let's see a real-world refactoring example. Consider a navigation component:
Before :has() (JavaScript Required)
<nav class="main-nav">
<ul class="nav-list">
<li class="nav-item has-submenu expanded">
<a href="#">Products</a>
<ul class="submenu">
<li><a href="#" class="active">Software</a></li>
<li><a href="#">Hardware</a></li>
</ul>
</li>
</ul>
</nav>
// JavaScript required to manage these classes
document.querySelectorAll('.nav-item').forEach(item => {
if (item.querySelector('.submenu')) {
item.classList.add('has-submenu');
}
if (item.querySelector('.active')) {
item.classList.add('expanded');
}
});
.nav-item.has-submenu > a::after {
content: "âź";
}
.nav-item.expanded > .submenu {
display: block;
}
After :has() (Pure CSS)
<nav class="main-nav">
<ul class="nav-list">
<li class="nav-item">
<a href="#">Products</a>
<ul class="submenu">
<li><a href="#" class="active">Software</a></li>
<li><a href="#">Hardware</a></li>
</ul>
</li>
</ul>
</nav>
/* No JavaScript needed! */
.nav-item:has(.submenu) > a::after {
content: "âź";
}
.nav-item:has(.active) > .submenu {
display: block;
}
.nav-item:has(.active) > a {
font-weight: bold;
color: var(--primary);
}
The benefits:
-
Cleaner HTML: No state classes
-
No JavaScript: Removes a runtime dependency
-
Automatic updates: Changes to DOM auto-update styles
-
Better performance: CSS is faster than JavaScript for this
Conclusion
The CSS :has() selector is more than a parent selectorâit's a paradigm shift in how we can express styling logic. After two decades of workarounds, we finally have a native way to select elements based on their relationships with other elements.
Key Takeaways
:has() is a relational pseudo-class, not just a parent selector. It can express sibling relationships, descendant conditions, and complex logical combinations.
Browser support is excellent at 95%+. You can use :has() today with simple fallbacks for legacy browsers.
Performance is generally not a concern for well-scoped selectors. Be specific, avoid deep nesting, and use direct child combinators when possible.
:has() reduces JavaScript dependency. Many interactive patterns that required JavaScript can now be pure CSS.
Combine with modern CSS features like nesting and container queries for even more powerful component-based styling.
The web platform continues to evolve, closing gaps that once required JavaScript workarounds. The :has() selector is one of the most significant additions to CSS in years, and mastering it will make you a more effective frontend developer.
Start smallâfind one JavaScript-dependent style pattern in your codebase and see if :has() can simplify it. You'll be surprised how often the answer is yes.
đĄ Note: This article was originally published on the Pockit Blog.
Check out Pockit.tools for 50+ free developer utilities (JSON Formatter, Diff Checker, etc.) that run 100% locally in your browser.