PureDevTools

CSS :has() Selector Reference

Interactive reference for the CSS :has() relational pseudo-class — parent selection, sibling selection, form states, and combining patterns

All processing happens in your browser. No data is sent to any server.

Options

Generated selector:

.card:has(img)

.card:has(img) — selects .card that has img as a any descendant

Generated CSS

/* .card that has a any descendant matching img */
.card:has(img) {
  /* Target styles here */
}

Common Examples

section:has(> h2)Section with a direct h2 child
.card:has(img)Card containing any image
label:has(+ input:required)Label before a required input
form:has(:invalid)Form with any invalid field
.card:not(:has(img))Card without an image (empty state)
li:has(> ul)List item with a nested list
.card:has(img):has(figcaption)Card with both image and caption
:is(article, section):has(> h2)Article or section with h2 child

Your card component should switch to a two-column layout when it contains an image, but keep a single-column layout when it’s text-only. Before 2023, this required JavaScript to detect the image and toggle a class. Now :has() does it in pure CSS — .card:has(img) { grid-template-columns: auto 1fr } — and it also unlocks parent selection, sibling-aware styling, and form validation states that were impossible without JS.

Why This Reference (Not Reading the MDN Page)

:has() is the most powerful CSS selector shipped in the last decade, but its full potential goes far beyond “parent selector.” It works with combinators (:has(> .direct-child)), negation (:has(:not(:checked))), siblings (:has(+ .next)), and compound selectors. This reference covers all the patterns with interactive examples — parent selection, sibling selection, form validation, combining with :not() and :is() — plus browser support notes. Everything runs in your browser.

What Is CSS :has()?

The CSS :has() pseudo-class is a relational pseudo-class that selects an element based on its descendants, direct children, or sibling elements. It is often called the “parent selector” because it was the first way in CSS to select a parent element based on its children.

/* Before :has() — required JavaScript */
// cardEl.classList.toggle('has-image', cardEl.querySelector('img') !== null);

/* With :has() — pure CSS */
.card:has(img) {
  display: grid;
  grid-template-columns: auto 1fr;
}

:has() accepts a relative selector list as its argument. The selector is evaluated relative to the element being tested, not relative to the document root.

Parent Selection

The most common use of :has() is selecting a parent based on its children:

/* Select section when it contains an h2 as a direct child */
section:has(> h2) {
  padding-top: 2rem;
  border-top: 2px solid #e5e7eb;
}

/* Select figure when it has a figcaption */
figure:has(figcaption) {
  display: grid;
  gap: 0.5rem;
}

/* Select li when it contains a nested list */
li:has(ul),
li:has(ol) {
  list-style: disclosure-open;
}

Descendant vs. Direct Child

Use the > combinator inside :has() to restrict the match to direct children only:

/* Any descendant img — matches any nesting depth */
.card:has(img) { }

/* Only a direct child img — must be immediate child */
.card:has(> img) { }

/* Only a direct child that is also a .featured img */
.card:has(> img.featured) { }

Sibling Selection

:has() can target siblings using the + (adjacent) and ~ (general) combinators inside its argument. This enables what was previously called “previous sibling selection”:

/* Style a label BEFORE a required input */
label:has(+ input:required)::after {
  content: " *";
  color: #ef4444;
  font-weight: 700;
}

/* Style a heading when a paragraph immediately follows it */
h2:has(+ p) {
  margin-bottom: 0.5rem;
}

/* Style nav items when any subsequent sibling is active */
.nav-item:has(~ .nav-item.active) {
  opacity: 0.6;
}

Form Validation States

Combine :has() with form pseudo-classes for pure-CSS validation UI:

/* Show error styles when field is invalid and touched */
.form-group:has(input:invalid:not(:placeholder-shown)) label {
  color: #ef4444;
}

.form-group:has(input:invalid:not(:placeholder-shown)) input {
  border-color: #ef4444;
}

/* Disable submit when any required field is invalid */
form:has(:invalid) [type="submit"] {
  opacity: 0.5;
  pointer-events: none;
}

/* Custom checkbox UI */
.checkbox-label:has(input:checked) {
  background: #eff6ff;
  border-color: #3b82f6;
}

Combining :has() with Other Selectors

AND Conditions — Multiple :has()

/* Card that has BOTH an image AND a figcaption */
.card:has(img):has(figcaption) {
  display: grid;
  grid-template-columns: 200px 1fr;
}

:not(:has()) — Absence Selection

/* Card that does NOT contain any image */
.card:not(:has(img)) {
  background: #f9fafb;
  min-height: 200px;
}

/* Form with NO invalid fields */
form:not(:has(:invalid)) [type="submit"] {
  background: #10b981;
}

:is() and :has()

/* Apply to multiple element types at once */
:is(article, section, aside):has(> h2) {
  margin-top: 3rem;
}

:where() and :has() — Zero Specificity

/* Apply :has() styles at zero specificity */
:where(.card, .panel):has(.badge) {
  position: relative;
}

Specificity

The specificity of :has() is determined by its most specific argument:

SelectorSpecificityExplanation
.card:has(img)(0,1,1)1 class + 1 type
.card:has(.badge)(0,2,0)1 class + 1 class
.card:has(#title)(1,1,0)1 id + 1 class
:where(.card):has(img)(0,0,1):where = 0 specificity + 1 type

Use :where(:has(X)) to reduce the specificity of :has() to zero.

Browser Support

CSS :has() is supported in all major modern browsers:

BrowserBasic :has():not(:has())Forgiving Selector
Chrome105+105+105+
Firefox121+121+121+
Safari15.4+15.4+15.4+
Edge105+105+105+
Opera91+91+91+

Global coverage: ~92% as of early 2026.

Forgiving Selector List

Unlike most CSS selector lists, :has() uses a forgiving selector list. An unsupported or invalid selector inside :has() is silently ignored rather than invalidating the entire rule:

/* Forgiving — if :unknown-pseudo doesn't exist,
   the :has(img) branch still works */
.card:has(img, :unknown-pseudo) {
  display: grid;
}

Checking for Support

@supports selector(:has(*)) {
  /* :has() is supported */
  .card:has(img) {
    display: grid;
  }
}

Common Patterns

Layout Switching by Content

.card { padding: 1.5rem; }

/* Two-column layout when image is present */
.card:has(> .card-image) {
  display: grid;
  grid-template-columns: 200px 1fr;
  padding: 0;
}

/* Extra padding for cards with code blocks */
.card:has(pre) {
  padding: 0;
}
/* Highlight nav wrapper when a link is active */
nav:has(a[aria-current="page"]) {
  border-bottom: 3px solid #3b82f6;
}

/* Highlight the dropdown that contains the active item */
.dropdown:has([aria-current="page"]) > .dropdown-toggle {
  color: #3b82f6;
  font-weight: 600;
}

Dark Mode with :has()

/* Apply dark theme based on a checkbox state (no JS needed) */
body:has(#dark-mode-toggle:checked) {
  background: #1f2937;
  color: #f9fafb;
}

Frequently Asked Questions

Is :has() the CSS parent selector?

Yes, effectively. :has() lets you select a parent element based on its children — something previously impossible in CSS. However, :has() is more general than just a parent selector: it can match based on siblings (:has(+ .sibling)) and arbitrary descendants, not just direct children.

Does :has() work inside @layer?

Yes. :has() works anywhere a selector is valid — inside @layer, @media, @supports, and nested CSS rules.

What is the performance impact of :has()?

:has() can be slightly more expensive than simple class selectors because the browser must examine descendants or siblings. In practice, for typical web pages with reasonable DOM sizes, the performance impact is negligible. For very large lists or frequently updated elements, use class-based approaches for performance-critical paths.

Can :has() be animated with CSS transitions?

Not directly. :has() selects elements statically based on DOM state. Transitions work normally on properties of elements matched by :has() — the transition fires when the matched state changes (e.g., when an input becomes :invalid).

How does :has() interact with Shadow DOM?

:has() does not cross Shadow DOM boundaries. It only looks within the same document context (or shadow root) as the element being tested.

Is :has() polyfillable?

There is no reliable pure-CSS polyfill, but JavaScript-based polyfills exist (e.g., css-has-pseudo). For most use cases, progressive enhancement (writing baseline styles without :has(), then adding :has() enhancements) is preferred over a polyfill.

Related Tools

More CSS Tools