:has() CSS Selector

Select parent elements based on their children — the “parent selector” CSS never had. Replaces JS DOM queries and class toggling for validation, layout, and state propagation.

1. Form Validation — form:has(:invalid)

The form border turns red and a warning appears when any field is invalid. Clear the required field or enter a valid email to see it turn green. Zero JS — replaces Bootstrap’s .was-validated class.

⚠ Form has invalid fields
✓ All fields valid

2. Card Layout Adaptation — .card:has(img)

Cards that contain an image automatically span 2 grid columns. No JS layout detection — pure CSS selector.

Text Only

A simple text card. Takes 1 column.

Placeholder
Card with Image

This card has an <img>, so :has(img) makes it span 2 columns.

Another Text Card

No image here. Single column.

3. Nav Active Highlight — .nav:has(.nav-link.active)

Click a nav link to make it active. The nav container gets a bottom border highlight via :has(.nav-link.active). The parent reacts to child state — no JS parent manipulation.

Try removing all active states — the bottom highlight disappears because :has(.nav-link.active) no longer matches.

4. Sibling-aware Spacing — h2:has(+ h3)

An <h2> followed immediately by an <h3> gets reduced margin and turns blue. The heading “knows” its next sibling via :has(+ h3). No JS DOM walking.

Section Title

Subtitle follows immediately

The h2 above has reduced margin and primary color because h2:has(+ h3) matches.

Standalone Section

This h2 has no h3 sibling, so normal styling applies.

Another Paired Section

Another subtitle

Same reduced margin applied automatically.

Implementation: :has() selector (key code)
/* Form validation — replaces .was-validated JS */
form:has(:invalid) {
  border-color: var(--bs-danger);
}
form:has(:invalid) .warning { display: block; }

/* Layout adaptation — replaces JS layout detection */
.card:has(img) {
  grid-column: span 2;
}

/* Parent state from child — replaces JS parent manipulation */
.nav:has(.nav-link.active) {
  box-shadow: 0 2px 0 0 var(--bs-primary);
}

/* Sibling awareness — replaces JS DOM walking */
h2:has(+ h3) {
  margin-bottom: 0.25rem;
  color: var(--bs-primary);
}