CSS traditionally selects an element from information about that element or its ancestors. The :has() relational pseudo-class adds a different capability: an element can match according to relative selectors evaluated from that element.

This makes many parent-aware and sibling-aware styles possible without adding state classes solely for presentation. The useful mental model is not “select the parent,” but “select this element if a related element matches a condition.”

Read :has() from the outside in

Consider a card that should use a different layout when it contains a direct image child:

.card:has(> img) {
  grid-template-columns: 8rem 1fr;
}

The subject being styled is still .card. The relative selector > img asks whether an img exists as a direct child of that card.

Removing the child combinator changes the meaning:

.card:has(img) {
  /* Matches when an image exists anywhere inside the card. */
}

Use the narrowest relationship that expresses the component contract. A direct-child selector is often safer when nested components may contain unrelated matching elements.

Select an element from what follows it

Because arguments to :has() are relative selectors, they can begin with sibling combinators. This lets CSS select an element according to later siblings.

For example, add spacing only when a heading is immediately followed by a paragraph:

h2:has(+ p) {
  margin-block-end: 0.5rem;
}

The + p selector is evaluated relative to each h2. The rule does not style the paragraph; it styles the heading whose next sibling is a paragraph.

A general-sibling relationship is also possible:

.section-title:has(~ .warning) {
  font-weight: 700;
}

This matches a .section-title that has a later .warning sibling under the same parent. Be careful with broad sibling searches in large or reusable containers because a distant match can affect an element unexpectedly.

React to form state without a helper class

Form pseudo-classes combine naturally with :has(). A field wrapper can respond when one of its controls is invalid:

.field:has(input:invalid) {
  border-color: currentColor;
}

Or a form can expose a state only after a control has received user interaction when an appropriate state selector is available for the desired behavior.

Keep validation semantics separate from decoration. CSS can reflect browser-exposed state, but it does not replace server-side validation or application logic.

It is also important to avoid assuming that :invalid means “the user just made a mistake.” A required empty control, for example, can already be invalid before submission. Choose form-state pseudo-classes according to the interaction you actually want.

Express component variants from content

Sometimes markup already contains enough information to choose a layout, making an extra modifier class redundant.

.notice {
  display: grid;
  gap: 0.75rem;
}

.notice:has(> .notice__icon) {
  grid-template-columns: auto 1fr;
}

This can be useful when the presence of a child is the source of truth. The component adapts automatically when the icon exists.

Do not use :has() merely to avoid every explicit class. A class such as .notice--critical is often clearer when the state represents application meaning rather than a structural fact visible in the DOM.

Combine multiple alternatives deliberately

:has() accepts a relative selector list, so one rule can test several relationships:

.article:has(> img, > video) {
  padding-block-start: 0;
}

This matches an article with a direct img or video child.

Keep the list understandable. If each condition represents a different design reason, separate rules may communicate intent better than one long selector.

Understand specificity before shipping

The :has() pseudo-class itself does not add a normal pseudo-class specificity weight. Its specificity contribution comes from the most specific selector in its argument list.

That means this selector can be stronger than it first appears:

.card:has(#featured-media) {
  /* The ID inside :has() contributes ID-level specificity. */
}

A later utility class may then fail to override the rule as expected.

Prefer classes and type selectors inside :has() when they are sufficient. If a design system needs intentionally low specificity, consider whether :where() can be used around suitable parts of the selector without changing the matching logic.

Avoid invalid constructions

:has() cannot be nested inside another :has():

/* Invalid */
.card:has(.body:has(img)) {
  display: grid;
}

Instead, express the relationship directly when possible:

.card:has(.body img) {
  display: grid;
}

Pseudo-elements are also not generally valid as :has() arguments unless a specification explicitly defines them as :has()-allowed pseudo-elements. Do not rely on constructions such as :has(::before) to detect generated content.

Keep selectors scoped to component boundaries

A selector such as this is powerful but broad:

body:has(.modal[open]) {
  overflow: hidden;
}

It asks the browser to match body according to a descendant state anywhere below it. The rule may be appropriate for a document-level behavior, but the dependency is global and should be obvious to maintainers.

For ordinary components, prefer a local anchor:

.product-card:has(> .product-card__actions) {
  padding-block-end: 0;
}

Local relationships are easier to reason about and less likely to acquire accidental matches as the page grows.

Use feature queries when fallback behavior matters

If the design must remain usable in an environment where a selector is unsupported, keep the baseline layout functional and layer the conditional enhancement separately.

.card {
  display: block;
}

@supports selector(.card:has(> img)) {
  .card:has(> img) {
    display: grid;
    grid-template-columns: 8rem 1fr;
  }
}

The feature query is useful when the enhanced rule materially changes layout. If the :has() rule only adds a minor decoration, doing nothing may already be an acceptable fallback.

Choose fallback policy from the product’s browser requirements rather than adding compatibility code automatically.

Common pitfalls

Treating :has() as application state management

Structural selectors are excellent when the DOM itself represents the condition. Explicit state attributes or classes are usually clearer when state comes from business logic, asynchronous data, or a state machine.

Using descendant matching when direct children are intended

.card:has(img) can match because of an image deep inside another nested component. Use .card:has(> img) when only the card’s own image should affect it.

Accidentally increasing specificity

A highly specific selector inside :has() raises the specificity of the whole selector. Check cascade behavior instead of assuming a relational selector is lightweight.

Hiding important semantics in CSS

A selector can react to DOM state, but CSS should not become the only place where important application rules are encoded. Accessibility state, validation logic, and authorization decisions belong in the appropriate HTML and application layers.

Building selectors that are hard to explain

If a :has() rule needs several combinators and many alternatives, an explicit class may be more maintainable. Selector cleverness is not a substitute for a clear component interface.

Choose :has() when the relationship is the state

The strongest uses of :has() are cases where the relationship between elements is already the truth you need to style: a component contains an optional child, a heading precedes particular content, or a wrapper reflects a control state.

Keep the anchor local, use precise combinators, watch specificity, and prefer explicit application state when the condition is not genuinely structural. Used this way, :has() removes presentation-only markup and classes without making the stylesheet harder to reason about.