CSS :has() Selector Is Finally Useful (Now That Safari Supports It)
The CSS :has() pseudo-class has been in browsers for a while now, but 2026 marks the point where it’s actually usable in production without polyfills or workarounds. Safari was the last major browser to implement it, and now that it’s universally supported, developers can finally tap into one of the most powerful CSS features added in years.
If you haven’t explored what :has() enables, you’re missing out on patterns that previously required JavaScript or convoluted workarounds. This isn’t just syntactic sugar—it fundamentally changes what’s possible with CSS selectors.
The Parent Selector We Always Wanted
The most obvious use case is selecting parent elements based on their children. For years, we wanted “parent selectors” but CSS only worked downward in the tree. :has() finally enables upward relationships.
/* Style a card differently if it contains an image */
.card:has(img) {
padding: 0;
}
/* Style a form differently if it has errors */
form:has(.error) {
border-color: red;
}
/* Style a list item based on its anchor having a specific class */
li:has(a.active) {
background: #f0f0f0;
}
These patterns required JavaScript previously. You’d add classes to parent elements based on child content. Now it’s pure CSS with no JS needed.
Conditional Styling Based on Element State
:has() works with any selector, including pseudo-classes and attribute selectors. This enables sophisticated conditional styling:
/* Style a button container differently if any button is disabled */
.button-group:has(button:disabled) {
opacity: 0.6;
}
/* Change form layout if any required field is empty */
form:has(input[required]:invalid) .submit-button {
opacity: 0.5;
pointer-events: none;
}
/* Highlight table rows containing cells with specific data */
tr:has(td[data-status="urgent"]) {
background: #fee;
}
This eliminates entire categories of JavaScript that existed solely to add classes for conditional styling.
Sibling Relationships and Gaps
Combining :has() with sibling selectors enables patterns that were previously very difficult:
/* Remove margin from the last child if it's followed by nothing */
.container > :has(+ *) {
margin-bottom: 1rem;
}
/* Style an element differently if it's NOT followed by a specific sibling */
h2:not(:has(+ p)) {
margin-bottom: 0.5rem;
}
/* Add dividers between specific elements only */
.item:has(+ .item) {
border-bottom: 1px solid #ccc;
}
The ability to conditionally style based on presence or absence of siblings simplifies many layout patterns.
Form Enhancement Without JavaScript
Forms are where :has() really shines. So many form UX patterns that needed JavaScript can now be pure CSS:
/* Show validation hints only when field is focused and invalid */
.form-group:has(input:focus:invalid) .hint {
display: block;
color: red;
}
/* Dim all labels except the one for the focused field */
form:has(input:focus) label {
opacity: 0.5;
}
form:has(input:focus) label:has(+ input:focus) {
opacity: 1;
}
/* Style submit button based on checkbox state */
form:has(input[type="checkbox"]:checked) .submit-button {
background: green;
}
These create better user experiences without the complexity and performance cost of JavaScript event listeners.
Count-Based Styling
You can use :has() to apply styles based on quantity of children, enabling designs that adapt to content:
/* Style grid differently based on number of items */
.grid:has(.item:nth-child(4)) {
grid-template-columns: repeat(2, 1fr);
}
.grid:has(.item:nth-child(7)) {
grid-template-columns: repeat(3, 1fr);
}
/* Hide "view all" link if there are fewer than 5 items */
.list:not(:has(.item:nth-child(5))) .view-all {
display: none;
}
This adapts layouts and UI elements based on content without JavaScript measuring.
Performance Considerations
:has() is more expensive than simple selectors because browsers need to check child/sibling relationships. For simple pages this doesn’t matter, but on complex pages with deep DOM trees and many elements, overuse of :has() can impact style calculation performance.
Use it where it provides value, but don’t replace every selector with :has() just because you can. Simple descendant selectors are still more performant when they suffice.
Browser implementations have optimized :has() significantly, but it will never be as cheap as simple class or ID selectors. Measure if you’re using it extensively.
Browser Support Is Finally There
As of early 2026, :has() works in all modern browsers:
- Chrome/Edge: Since version 105 (August 2022)
- Firefox: Since version 103 (July 2022)
- Safari: Since version 15.4 (March 2022)
The holdout was older Safari versions on iOS devices that haven’t updated, but by 2026 usage of those versions is low enough that most sites can rely on :has() support.
For projects that need to support older browsers, feature detection and progressive enhancement work well. The absence of :has() support degrades gracefully—the enhanced styling simply doesn’t apply.
Combining :has() With Other Modern CSS
:has() becomes even more powerful combined with other modern CSS features:
/* Use with container queries for component-aware styling */
@container (min-width: 400px) {
.card:has(img) {
display: grid;
grid-template-columns: 200px 1fr;
}
}
/* Combine with :is() for complex selector groups */
article:has(:is(h2, h3) + figure) {
padding-top: 2rem;
}
/* Use with logical properties for internationalization */
.sidebar:has(+ .main-content) {
margin-inline-end: 2rem;
}
The combination of :has(), container queries, and other modern CSS features enables sophisticated responsive designs with minimal JavaScript.
Common Pitfalls to Avoid
Don’t nest :has() within itself excessively. div:has(.foo:has(.bar)) works but gets expensive and hard to maintain. Flatten your selectors when possible.
Remember specificity. :has() itself doesn’t add specificity, but the selectors inside it do. div:has(.active) has the specificity of a class selector, not a type selector.
Watch for unintended matches. :has() matches based on descendants at any depth unless you specify direct children with >. Be specific about what you’re checking.
Real-World Patterns I’m Actually Using
Card layouts that adapt based on content:
.card:has(img) {
display: grid;
grid-template-areas: "image content";
}
.card:not(:has(img)) .content {
padding: 2rem;
}
Form sections that show/hide based on checkbox state:
.conditional-section {
display: none;
}
form:has(#show-advanced:checked) .conditional-section {
display: block;
}
Navigation that highlights based on active page:
nav li:has(a[aria-current="page"]) {
border-left: 3px solid blue;
background: #f5f5f5;
}
Resources for Learning More
The MDN documentation for :has() is comprehensive and includes good examples. Ahmad Shadeed has written extensively about practical :has() patterns worth reading.
Practice in your browser DevTools. Type selectors using :has() in the console and see what they match. The immediate feedback helps build intuition.
Look for JavaScript in your codebase that adds/removes classes based on element state or presence of children. Many of those patterns can now be pure CSS with :has().
Start Using It
If you’ve been waiting for :has() to be production-ready, 2026 is the year. Browser support is universal, performance is acceptable, and the patterns it enables genuinely improve both developer experience and user experience.
Start small. Replace one JavaScript-based conditional styling pattern with :has(). See how it feels. The ergonomics of handling these relationships in CSS rather than JavaScript are noticeably better once you get used to the syntax.
Not every project needs :has(), but most projects have at least a few spots where it simplifies code and improves maintainability. It’s worth adding to your CSS toolkit and reaching for when appropriate.