The Historical Limitation: Why CSS Couldn't Select Previous Siblings
For years, CSS developers faced a frustrating asymmetry in the language. While the adjacent sibling combinator (+) made it trivial to select elements that follow other elements, there was no built-in way to target elements that came before the current element in the DOM tree. This limitation shaped how developers approached layout and interaction design, often requiring creative workarounds or JavaScript interventions.
The CSS specification was designed this way intentionally--selectors work in document order, flowing from parent to children, from earlier to later siblings. While this made the selector engine simpler and more performant, it created real challenges for common UI patterns. As modern web development evolved, the need for more flexible selector capabilities became increasingly apparent, driving innovation in CSS specification and browser support.
With the introduction of the :has() pseudo-class, that limitation has been eliminated. This breakthrough enables developers to create sophisticated interactive layouts without relying on JavaScript for styling decisions that can now be handled entirely in CSS.
Understanding CSS Sibling Combinators
Before diving into the :has() solution, it's essential to understand the sibling combinators that have always existed in CSS and how they work.
The Adjacent Sibling Combinator
The adjacent sibling combinator (+) selects an element that immediately follows another element with the same parent. This is the most common sibling selector:
/* Select paragraphs that immediately follow an h2 */
h2 + p {
margin-top: 0;
}
/* Style a card that immediately follows another card */
.card + .card {
border-left: 1px solid #e0e0e0;
}
The General Sibling Combinator
The general sibling combinator (~) selects all siblings that follow an element, not just the immediate one:
/* Select all paragraphs that follow an h2 */
h2 ~ p {
color: #666;
}
Both combinators share a critical limitation: they only look forward in the DOM, never backward. This is where :has() changes everything. For developers building responsive web applications, understanding these fundamentals is crucial for creating maintainable stylesheets that scale with your web development projects.
The Game-Changer: CSS :has() Pseudo-Class
The :has() pseudo-class represents a paradigm shift in what CSS selectors can do. Rather than selecting elements directly, :has() selects elements based on what they contain or what follows them. This "relational selector" capability is what makes previous sibling selection possible.
How :has() Works
The :has() pseudo-class takes a relative selector as its argument. If any element matching that selector exists in relationship to the current element, the current element gets selected:
/* Select sections that contain a featured article */
section:has(.featured) {
border: 2px solid #3b82f6;
}
/* Select list items that have a nested list */
li:has(ul) {
list-style-type: disc;
}
Browser Support
The :has() pseudo-class reached Baseline availability in December 2023, meaning it's now supported across all major browsers:
- Chrome/Edge: Version 105+ (September 2022)
- Safari: Version 15.4+ (March 2022)
- Firefox: Version 121+ (December 2023)
You can confidently use :has() in production for modern web development projects, with optional feature detection for graceful degradation. According to MDN Web Docs, the :has() selector provides powerful new capabilities while maintaining strong performance characteristics across modern browser engines.
:has() Browser Support
100%
Major Browser Support
2023
Baseline Availability
3
Major Engines
Selecting the Immediate Previous Sibling
The key to selecting previous siblings lies in combining :has() with the adjacent sibling combinator. The pattern looks like this:
/* Select elements whose next sibling is a .highlight element */
.element:has(+ .highlight) {
background-color: #fef3c7;
}
This selector works by:
- Finding all
.elementcandidates - Checking if any of them has a
.highlightas its immediate next sibling - Only those matching elements receive the styling
Practical Example
Consider a card layout where you want to highlight the card that appears before a featured card:
<div class="card">Card 1</div>
<div class="card">Card 2</div>
<div class="card featured">Featured Card</div>
<div class="card">Card 4</div>
/* Highlight the card immediately before a featured card */
.card:has(+ .featured) {
border: 2px solid #f59e0b;
box-shadow: 0 4px 6px rgba(245, 158, 11, 0.3);
}
In this example, "Card 2" gets highlighted because it's immediately followed by the featured card. This technique is particularly valuable when building interactive card components where visual relationships between elements matter.
The Mental Model
Think of :has(+ .target) as asking: "Does this element have a .target as its next sibling?" If yes, style this element. From the perspective of the .target, this is its previous sibling. This reverse-think approach becomes intuitive with practice, as noted in Tobias Ahlin's comprehensive guide on previous sibling selection.
1/* Select the element immediately before a .featured element */2.card:has(+ .featured) {3 background-color: #fef3c7;4 border-color: #f59e0b;5}6 7/* From the perspective of .featured, this is its previous sibling */8/* But CSS can't select 'previous' directly, so we reverse the logic */Selecting the Nth Previous Sibling
What if you need to select an element that's two siblings back? Or three? You can chain additional adjacent combinators to reach further back:
/* Select the element immediately before a .target (1 back) */
.element:has(+ .target) { }
/* Select the element 2 siblings before a .target */
.element:has(+ * + .target) { }
/* Select the element 3 siblings before a .target */
.element:has(+ * + * + .target) { }
How It Works
Each + in the selector advances one position forward:
+ .target= immediately after (1 position)+ * + .target= skip one element, then find target (2 positions back)+ * + * + .target= skip two elements, then find target (3 positions back)
Practical Use Case: Card Carousel
A common application is creating interactive card carousels where hovering over one card affects its neighbors:
/* The hovered card */
.card:hover {
transform: scale(1.05);
z-index: 10;
}
/* Card immediately after the hovered card */
.card:hover + .card {
transform: scale(1.02);
}
/* Card immediately before the hovered card */
.card:has(+ .card:hover) {
transform: scale(1.02);
}
This creates a symmetrical effect where cards on both sides of the hovered card subtly scale up. As demonstrated by Frontend Masters, this pattern is essential for creating polished interactive web experiences without JavaScript.
Selecting All Preceding Siblings
The general sibling combinator (~) combined with :has() allows you to select all elements that precede a target element anywhere in the sibling chain:
/* Select all .box elements that have a .circle somewhere after them */
.box:has(~ .circle) {
background-color: #bfdbfe;
}
Combining Patterns
You can get more specific by combining the general and adjacent combinators:
/* Select all .box elements that have a .circle after them,
but NOT as the immediate next sibling */
.box:has(~ * + .circle) {
background-color: #bfdbfe;
}
This pattern is useful when you want to style elements conditionally based on content that appears later in the document.
Form Validation Example
A practical application is highlighting form sections based on validation state, which is crucial for building accessible web applications:
/* Highlight input labels when the input has an error */
.input-group:has(.input[aria-invalid="true"]) .label {
color: #dc2626;
}
/* Style all inputs after a required field that is empty */
.input:has(:placeholder-shown) ~ .input {
opacity: 0.7;
}
These CSS-only validation styles reduce the need for JavaScript and improve performance for progressive web applications where fast interaction feedback is essential.
Performance Considerations
While :has() opens new possibilities, it's important to understand its performance implications.
How :has() Affects Rendering
The :has() pseudo-class is evaluated during the selector matching phase, which can trigger style recalculation. In complex documents, deeply nested :has() selectors or multiple :has() conditions can impact rendering performance. Modern browser engines have optimized :has() evaluation, but understanding best practices ensures your web applications remain performant.
Best Practices
- Keep selectors simple: Avoid deeply nested :has() conditions
- Be specific: Use class selectors instead of element selectors when possible
- Limit scope: Apply :has() to specific containers, not the entire document
- Test on lower-end devices: Performance may vary across devices
Specificity Rules
The :has() pseudo-class takes the specificity of its most specific argument, similar to :is() and :not():
/* Specificity of the most specific argument (.button) */
.container:has(.button:hover) {
/* Specificity: 0,1,1 (one class, one pseudo-class) */
}
/* Specificity of 0,2,0 (two classes) */
:has(.sidebar .nav) {
/* Specificity from .sidebar .nav = 0,2,0 */
}
Graceful Degradation
For projects supporting older browsers, use @supports for feature detection:
/* Only apply :has() styles in supporting browsers */
@supports (selector(:has(.child))) {
.card:has(+ .featured) {
/* Styles for browsers with :has() support */
}
}
/* Fallback for older browsers */
.card.highlight-previous {
/* Fallback styles */
}
Following these performance guidelines ensures your CSS remains maintainable while leveraging modern capabilities for high-performance websites.
Frequently Asked Questions
Can :has() be nested?
No, :has() cannot be nested within another :has(). Attempting to do so will invalidate the selector and browsers will ignore the entire rule.
Does :has() work with pseudo-elements?
No, pseudo-elements are not valid selectors within :has() and cannot serve as anchors for :has(). This prevents cyclic queries and maintains selector performance.
Is :has() performant for complex layouts?
:has() is generally performant for most use cases. However, very complex selectors or multiple :has() conditions on large documents may impact rendering. Test on target devices for critical applications, especially for complex [single-page applications](/solutions/single-page-applications/).
What's the difference between :has(+) and :has(~)?
:has(+) selects elements with an immediate next sibling matching the selector. :has(~) selects elements with any following sibling matching the selector, anywhere in the sibling chain.
Can I select a parent's previous sibling?
Yes, :has() can select parent elements based on their children, and combining this with sibling selection allows for complex ancestor/sibling patterns that would previously require JavaScript.
Conclusion
The :has() pseudo-class represents one of the most significant additions to CSS in recent years, finally allowing developers to select previous siblings and parent elements without JavaScript. This capability enables cleaner, more maintainable code for common UI patterns like:
- Interactive layouts: Card carousels with neighbor awareness
- Form validation: Conditional styling based on sibling states
- Dynamic content: Styling elements based on content that follows
- Accessible interfaces: Providing visual feedback without extra markup
With Baseline support across all major browsers since late 2023, :has() is now ready for production use. Start incorporating it into your projects to reduce JavaScript dependencies and create more sophisticated CSS-only interfaces that perform excellently on responsive websites and progressive web applications.
Quick Reference
| Pattern | Selector | Purpose |
|---|---|---|
| Immediate Previous | :has(+ .target) | Element immediately before target |
| Nth Previous | :has(+ * + .target) | Element N positions before target |
| All Preceding | :has(~ .target) | All elements before target |
| Exclude Immediate | :has(~ * + .target) | Elements before, not immediately before |
Sources
- MDN Web Docs - :has() Selector - Official documentation and browser support information
- Tobias Ahlin Blog - Selecting previous siblings with CSS :has() - Comprehensive coverage of all previous sibling patterns with interactive examples
- Frontend Masters Blog - Selecting Previous Siblings - Code examples for adjacent sibling combinators and :has() patterns