🚀 LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
🎓 COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
CSS MASTER CLASS /// VISUAL ENGINEERING /// LAYOUT DESIGN /// ANIMATION LAB /// CSS MASTER CLASS /// VISUAL ENGINEERING ///

:has() — CSS's Long-Awaited Parent Selector

Learn how :has() matches a parent based on its descendants, how it combines with sibling combinators and :not() for absence checks, and how its specificity and performance characteristics work.

Total XP: 0|💻 css XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

:has() Selector

CSS's relational parent selector.


🚀 LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
🎓 COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

CSS selectors have always described a path downward or sideways through the DOM — never upward. :has() breaks that constraint, letting you style an element based on what exists inside, next to, or notably absent from it.

1The Feature CSS Was Missing For Two Decades

Every other CSS selector describes a relationship in one direction: a descendant combinator selects something *inside* an ancestor, a sibling combinator selects something *after* a sibling. None of them let you select an ancestor *based on* what's inside it — that always required JavaScript to inspect the DOM and toggle a class manually.

:has() closes this gap directly. .card:has(img) reads naturally: select a .card, but only if it has an <img> inside it. The element ultimately styled is the one before :has(), not the one inside its argument — a genuinely new kind of relationship CSS selectors can now express.

.card:has(img) {
  grid-template-columns: 120px 1fr;
}
localhost:3000
✓ True Parent SelectionOnly .card elements that actually contain an image get the image-aware two-column layout — cards without one keep the default single-column layout.

2Practical Patterns: Validation, Empty States, Siblings

The most immediately useful real-world pattern is form validation styling: form:has(:invalid) lets a form container show an error state the moment any field inside becomes invalid, with zero JavaScript. The inverse pattern, .list:not(:has(li)), detects an empty list by checking for the *absence* of any list item — useful for showing a placeholder without a script tracking list length.

:has() also composes with sibling combinators: h2:has(+ p) matches an <h2> only when immediately followed by a <p>, letting typography rules adapt based on what comes next in the document, another relationship previously impossible in pure CSS.

form:has(:invalid) { border-color: #dc2626; }
.list:not(:has(li))::after { content: "No items yet"; }
localhost:3000
⚠ Form Has Invalid Fieldsborder-color: #dc2626 applied to the whole form the instant any descendant input becomes invalid — no JS listener required.

3Specificity Rules And Sensible Performance Boundaries

:has() follows the same specificity model as :is() and :not(): the pseudo-class itself contributes zero specificity on its own, but the selector(s) inside its argument list contribute their specificity, with the highest one counted. .card:has(#featured) therefore carries the specificity of an ID selector, even though .card alone would normally be much lower.

On performance, modern browser engines have invested specifically in optimizing :has(), and typical use cases (checking a handful of children) are fast. The caution applies to genuinely unbounded queries — using :has() to search deep, large subtrees on every style recalculation is measurably more expensive than a direct selector, so it's worth reserving for conditions that are genuinely relational rather than reaching for it as a default habit.

/* Specificity driven by the argument, not :has() itself */
.card:has(#featured) { border: 2px solid gold; }
localhost:3000
Specificity of .card:has(#featured):
= specificity of #featured (1,0,0)

4Step-by-Step Breakdown

The Parent Selector CSS Never Had. For over two decades, 'style the parent of X' was the single most requested CSS feature, and the answer was always 'use JavaScript'. The :has() relational pseudo-class finally closes that gap: it matches an element if any selector passed to it matches something inside — including descendants, siblings, or even nothing at all.

Matching A Parent By Its Contents. :has() is applied to the element you want to select, and its argument describes what must exist relative to it. .card:has(img) selects a .card that contains an <img> anywhere inside it — the selector's target and its condition are on opposite ends of the relationship for the first time in CSS.

The Parent Selector. What does .card:has(img) select?

  • Every <img> element inside a .card
  • Every .card element that contains an <img> anywhere inside it
  • Every element immediately after a .card containing an img

Beyond Descendants: Siblings And Absence. :has() isn't limited to descendant checks. Combined with sibling combinators it can express 'style this element if a specific sibling follows it', and combined with :not() it can express absence — form:has(:invalid) for a form containing any invalid field, or .list:not(:has(li)) for an empty list.

Sibling And Absence Checks. What does form:has(:invalid) match?

  • Every invalid field, regardless of its form
  • The <form> element itself, if it contains at least one currently invalid field
  • Only forms where every field is valid

A Meaningful Specificity And Performance Note. :has() takes on the specificity of its most specific argument, just like :is(). Performance-wise, browsers optimize it well for common cases, but an unbounded :has(*) style query over a huge subtree is measurably more expensive than a plain selector — reserve it for genuinely relational conditions, not as a blanket replacement for simpler selectors.

:has() Specificity. How does :has() contribute to a selector's overall specificity?

  • It always contributes zero specificity, like :where()
  • It contributes the specificity of the most specific selector inside its argument
  • It always contributes a fixed, maximum specificity regardless of its argument

Relational Selection Unlocked. You can now select an element based on its descendants, siblings, or their absence — the single most-requested CSS capability for over twenty years, delivered without a single line of JavaScript. Use it deliberately for genuinely relational conditions like form validation states or content-aware layouts.

Style A Parent Based On Its Children. :has() lets a selector match a parent based on what it contains — a real 'parent selector' at last.

Level Up 🚀

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported.

FirefoxSupported

Fully supported.

SafariSupported

Fully supported.

EdgeSupported

Fully supported.

Accessibility (A11y)

1:has() Can Replace JavaScript-Toggled Classes For Purely Visual State, Reducing Script Complexity

Using form:has(:invalid) instead of a JS-added .has-error class means one less place visual state and actual validity can drift out of sync, since the CSS reads live DOM validity state directly.

2:has()-Driven Style Changes Must Still Be Paired With Accessible Error Messaging

Styling a form's border red when it :has(:invalid) fields is a visual-only signal; it must be paired with programmatically associated error text (via aria-describedby) so the same information reaches screen reader users.

SEO Implications

  • 1

    :has() Can Eliminate JavaScript Previously Required Purely For Conditional Styling

    Replacing a ResizeObserver or DOM-inspection script with a native :has() rule reduces JavaScript bundle size and main-thread work, which are both inputs into Core Web Vitals metrics search engines weigh.

  • 2

    Unbounded :has() Queries On Very Large Pages Can Measurably Affect Style Recalculation Time

    On pages with thousands of DOM nodes, broad :has() conditions checked against large subtrees can add to layout/style recalculation cost, worth profiling on content-heavy pages before shipping broadly.

Best Practices

Reach For :has() To Replace JavaScript DOM-Inspection Hacks First

Many existing 'parent selector' JavaScript workarounds (adding a class to a parent based on child state) can be deleted entirely and replaced with a single :has() rule, reducing both code and a class of state-sync bugs.

Scope :has() Queries As Narrowly As Reasonably Possible

Prefer .form:has(:invalid) over body:has(.form :invalid) — narrower relational scope is both clearer intent and cheaper for the browser to evaluate repeatedly.

Frequent Bugs

THE BUG

A form's error-state border doesn't clear after a user fixes the invalid field.

THE FIX

:has(:invalid) is live and re-evaluates automatically — if it's not clearing, the field's actual validity (not just its visual appearance) likely still fails HTML validation constraints.

THE BUG

A broad :has() selector is suspected of causing a noticeable style recalculation slowdown on a large page.

THE FIX

Narrow the :has() scope to a smaller, more specific ancestor instead of querying from a very high-level container across the whole subtree.

Real-World Examples

Zero-JavaScript Form Validation Styling

A signup form that highlights itself in red the instant any required field is left invalid, using only native HTML validation attributes and a single :has() rule.

form:has(:invalid:not(:placeholder-shown)) {
  border: 2px solid #dc2626;
}

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Using an overly broad :has() query across a very large ancestor

/* Costly */ body:has(.form input:invalid) { } /* Scoped */ .form:has(input:invalid) { }

The Solution //

Scope the :has() condition to the narrowest sensible ancestor rather than a high-level container like body.

The Error //

Relying on :has() alone to communicate form errors, with no accessible error text

<input aria-describedby="email-error" aria-invalid="true"> <span id="email-error">Enter a valid email</span>

The Solution //

Pair the visual :has() styling with an aria-describedby error message so screen reader users get the same information.

Lesson Glossary

[01]:has()

A relational pseudo-class matching an element based on its contents.

Code Preview
.card:has(img)

[02]Relational Selector

A selector expressing a relationship between elements, like :has().

Code Preview
Parent selector

[03]Absence Check

Using :not(:has()) to detect a missing descendant.

Code Preview
.list:not(:has(li))

[04]Live Re-evaluation

:has() automatically updates as the DOM or validity state changes.

Code Preview
form:has(:invalid)

Continue Learning