Field Guide / advanced

Stateful UI with :has() without turning CSS into business logic

The :has() selector is powerful when it reflects document state, not hidden application rules.

:has() lets CSS style an element based on what it contains. That makes parent-aware styling possible without adding a class in many cases.

The temptation is to use it everywhere. The safer rule is this: use :has() when CSS is responding to visible document state, not when it is trying to own application logic.

Good use: form state

.field:has(input:invalid) {
  --field-border: #d85d3f;
}

.field:has(input:focus-visible) {
  --field-border: #0f8b8d;
}

The field is styling itself based on the actual state of its input. There is no hidden data model. The selector mirrors the document.

Good use: content-aware layout

.media-card:has(img) {
  grid-template-columns: 10rem minmax(0, 1fr);
}

This can be reasonable when the card’s layout really depends on whether media exists. The markup already contains that fact.

Risky use: product rules

.plan:has([data-tier="enterprise"]) {
  order: -1;
}

This may work, but it hides a product decision in CSS. If enterprise plans should sort first, the data or template should own that order. CSS can style the result.

Performance and readability

Modern engines support :has(), but selector clarity still matters. A rule like .app:has(main article .card form input:checked) is hard to debug because it ties distant parts of the document together. Keep :has() close to the component boundary.

A useful team guideline

If the selector can be explained as “style this component when it contains this visible state,” it is probably a good fit. If it sounds like “change product behavior when a distant data condition exists,” move the decision out of CSS.

References