CSS :has() Parent Selector and Relational Queries

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.

: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”.

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().

:has() can accept almost any compound selector within it, enabling flexible matching.

figure:has(img)          /* figure containing <img> */
article:has(#featured)   /* article containing element with id="featured" */
li:has(a[target="_blank"])
div:has(a[href*="toulan.fun"])
form:has(:focus-visible)
label:has(input:checked)
section:has(> h2:first-child)

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.

Use CaseExample Code
Parent element depends on child state.menu li:has(> a.active)
Adjacent relationship matchingh1:has(+ h2)
Form validationform:has(input[required]:invalid)
Attribute-based selectiondiv:has(a[target="_blank"])
State-based selectionlabel:has(input:checked)
Global controlhtml:has(dialog[open])
.menu li:has(> a.active) {
  background-color: whitesmoke;
}
.grid:has(.grid-item:nth-child(7)) {
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
html:has(dialog[open]) {
  overflow: hidden; /* Prevent scrolling when dialog is open */
}
// 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)');
const available = page.locator(
  '.product-card:has(button.add-to-cart):not(:has(.out-of-stock-label))'
);
await available.first().click();
/* 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;
}

Despite :has() being very powerful, it has significant matching overhead. Usage recommendations:

  1. Limit scope: Use based on specific containers rather than global wildcards.

    /* ✅ Recommended */
    .nav li:has(> a.active)
    
    /* ❌ Use with caution */
    :has(.active)
  2. Prioritize explicit relationships: Use >, +, ~ to limit scope, avoid deep descendant selectors.

  3. Keep conditions simple: Excessive selector combinations increase rendering pressure.

  4. Watch specificity: Avoid overly high specificity in :has() selectors that make styles difficult to override.

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.