Hi, I’m HenryZ 👋.
In frontend development, we often encounter these requirements:
- Parent element styles depending on child element states — Such as navigation bars where parent
<li>needs to highlight based on whether child<a>has.active. - Layout changes with content — For example, CSS Grid containers switching column numbers when child elements exceed a threshold.
- Interaction-driven parent styles — During form validation, parent
<form>changes state based on the validity of internal<input>elements.
Previously, these scenarios could only rely on JavaScript: traversing the DOM to query child elements, then manually adding classes to parent elements. This not only increased code volume but also scattered logic between CSS and JS, reducing maintainability.
Now, all major browsers support the CSS :has() pseudo-class selector. It gives CSS the ability for “upward selection” and “relational queries”, allowing many logics that originally required scripting to be completed directly in stylesheets.
📖 Background Knowledge
:has() originates from the W3C Selectors Level 4 specification, and is an important enhancement following conditional selectors like :is() and :not().
In traditional CSS, selectors are unidirectional: they can only match from top to bottom (parent → child), unable to select parent elements based on child element states. This is known as the Parent Selector Problem.
With browser performance and selector engine optimizations, modern browsers now support :has():
- Chrome ≥ 105
- Safari ≥ 15.4
- Edge ≥ 105
- Firefox ≥ 121
Therefore, the emergence of :has() is not just a syntax extension, but a key turning point for CSS from “static description” to “relationship-driven”.
⚙️ Core Syntax and Rules
The basic usage of :has() is: Select parent or current elements based on conditions of child or sibling elements.
/* Parent li containing direct child .active */
li:has(> .active)
/* h1 immediately followed by h2 */
h1:has(+ h2)
/* form containing invalid required input */
form:has(input[required]:invalid)Core rules:
- Selection subject: Always the element to the left of
:has(). - Relational parameters: Selectors within parentheses set conditions; invalid ones are ignored.
- Specificity: Determined by the highest specificity of selectors within
:has(), consistent with:is()and:not().
🧩 Building Complex Conditions
:has() can accept almost any compound selector within it, enabling flexible matching.
By Element or Class
figure:has(img) /* figure containing <img> */
article:has(#featured) /* article containing element with id="featured" */By Attributes
li:has(a[target="_blank"])
div:has(a[href*="toulan.fun"])By State or Structure
form:has(:focus-visible)
label:has(input:checked)
section:has(> h2:first-child)By Content (Using Pseudo-classes)
Strictly speaking, CSS selectors cannot directly match “text content”. However, some scenarios can be indirectly implemented through :contains() (experimental feature in some browsers or third-party extensions), [attr*="text"], etc.
For example:
/* Only available in environments supporting :contains() */
a:has(:contains("Buy Now"))
/* Simulate text matching using aria-label */
button:has([aria-label*="Submit"])⚠️ Note: Native CSS does not yet officially support matching based on pure text content. Common practices utilize attribute values or JS assistance.
📊 :has() Usage Quick Reference
| Use Case | Example Code |
|---|---|
| Parent element depends on child state | .menu li:has(> a.active) |
| Adjacent relationship matching | h1:has(+ h2) |
| Form validation | form:has(input[required]:invalid) |
| Attribute-based selection | div:has(a[target="_blank"]) |
| State-based selection | label:has(input:checked) |
| Global control | html:has(dialog[open]) |
🎨 Styling Layer Applications
Parent Styles Responding to Child States
.menu li:has(> a.active) {
background-color: whitesmoke;
}Layout Dynamically Changing with Content
.grid:has(.grid-item:nth-child(7)) {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}Global State Control
html:has(dialog[open]) {
overflow: hidden; /* Prevent scrolling when dialog is open */
}🚀 Cross-Domain Application Scenarios
JavaScript DOM Queries
// Traditional approach
const checkedLabels = Array.from(document.querySelectorAll('label'))
.filter(label => label.querySelector('input:checked'));
// Using :has()
const checkedLabels = document.querySelectorAll('label:has(input:checked)');Automated Testing and Web Scraping
const available = page.locator(
'.product-card:has(button.add-to-cart):not(:has(.out-of-stock-label))'
);
await available.first().click();Ad Blocking and Content Filtering
/* Hide divs containing ad links */
div:has(a[href*="doubleclick.net"]) {
display: none !important;
}
/* Hide sections without core content */
main section:not(:has(p, img, h2, ul)) {
display: none;
}⚡️ Performance Optimization and Best Practices
Despite :has() being very powerful, it has significant matching overhead. Usage recommendations:
Limit scope: Use based on specific containers rather than global wildcards.
/* ✅ Recommended */ .nav li:has(> a.active) /* ❌ Use with caution */ :has(.active)Prioritize explicit relationships: Use
>,+,~to limit scope, avoid deep descendant selectors.Keep conditions simple: Excessive selector combinations increase rendering pressure.
Watch specificity: Avoid overly high specificity in
:has()selectors that make styles difficult to override.
🎯 Summary
CSS :has() brings parent selection and relational query capabilities to selectors, breaking through long-standing unidirectional limitations.
It’s particularly valuable in:
- CSS styling control — Parent elements dynamically change with child states.
- JavaScript DOM queries — More semantic and maintainable code.
- Automated testing and web scraping — Precise positioning in complex scenarios.
- Browser extensions and content filtering — Efficient expression of content matching logic.
As frontend developers, we should include :has() in our modern toolkit, using it reasonably to simplify logic and improve maintainability, while always paying attention to performance and compatibility.