How Does the CSS :has Pseudo-Class Work?

The :has() pseudo-class is a relational selector that enables developers to style an element based on its descendants, child elements, or following siblings. Often referred to as the long-awaited "parent selector," :has() checks whether an element contains or is adjacent to specific targets before applying styles. This article explores how :has() functions, breaks down its syntax and specificity rules, and highlights practical design patterns that previously required JavaScript.

Understanding Relational Selection

Traditionally, CSS cascade logic flows in one direction: top-down and left-to-right. Standard selectors let you target an element's children (parent > child) or subsequent siblings (item + item), but selecting a container based on what it contains was impossible using pure CSS.

The :has() pseudo-class fundamentally alters this dynamic. It accepts a relative selector list as an argument and matches the subject element if any of the selectors in the argument match relative to that subject.

/* Styles the <article> ONLY if it contains an <img> */
article:has(img) {
  display: grid;
  grid-template-columns: 1fr 2fr;
}

In the example above, article is the target being styled, not the img. The browser evaluates the argument img relative to article. If true, the grid layout applies to the article container.

Key Syntax Patterns

The :has() selector supports complex combinators, multiple conditions, and logical chains.

1. Direct Child Matching

Using the child combinator (>) inside :has() ensures styles only apply when the target is an immediate child rather than a deeply nested descendant.

/* Targets a card only if a header is its direct child */
.card:has(> h2) {
  border-top: 4px solid var(--accent-color);
}

2. Sibling Relationships

Because :has() accepts relative combinators, you can evaluate adjacent siblings (+) and general siblings (~).

/* Styles a label when its immediately preceding checkbox is checked */
label:has(+ input:checked) {
  font-weight: 700;
  color: var(--primary);
}

/* Styles an H2 only if it is immediately followed by a subtitle paragraph */
h2:has(+ p.subtitle) {
  margin-bottom: 0.25rem;
}

3. Logical Operations and Inversion

:has() can be combined with :not() and :is() to construct sophisticated state-based styling logic.

/* Matches a form group that has an invalid input but is NOT focused */
.form-group:has(input:invalid):not(:focus-within) {
  border-color: var(--error-red);
}

/* Matches a container that contains either a video or an audio player */
.media-wrapper:has(video, audio) {
  background-color: var(--dark-bg);
}

Practical Use Cases

Dynamic Component Layouts

Components often require different visual treatments depending on their internal contents. Without :has(), developers typically toggle modifier classes like .card--has-image using client-side scripts. With :has(), CSS handles this declaratively:

.card {
  padding: 1.5rem;
  background: white;
}

/* Remove top padding if the hero image sits at the top */
.card:has(> img:first-child) {
  padding-top: 0;
}

Real-Time Form State Feedback

You can apply global layout adaptations when specific form controls change state.

/* Highlight a submit button when all required inputs inside the form are valid */
form:has(input:required:valid) button[type="submit"] {
  background-color: var(--success-green);
  cursor: pointer;
}

Specificity and Performance Considerations

Understanding how :has() interacts with the CSS cascade ensures predictable style sheets.