Skip to content

:has() Selector

The :has() relational pseudo-class allows selecting elements based on their descendants, enabling “parent selectors” and complex relational styling that previously required JavaScript.

<!-- Adding classes via JavaScript -->
<div class="form-group has-error">
<label>Email</label>
<input type="email" class="error" />
<span class="error-message">Invalid email</span>
</div>
// JavaScript to add parent classes
document.querySelectorAll('input').forEach((input) => {
input.addEventListener('invalid', () => {
input.closest('.form-group').classList.add('has-error');
});
input.addEventListener('input', () => {
if (input.validity.valid) {
input.closest('.form-group').classList.remove('has-error');
}
});
});
/* Styling based on JavaScript-added class */
.form-group.has-error label {
color: red;
}

Problems:

  • JavaScript required for parent-based styling
  • Extra classes clutter the markup
  • Event listeners add complexity
  • State synchronization issues
  • Performance overhead
/* Style parent based on child state - no JavaScript! */
.form-group:has(input:invalid) label {
color: red;
}
.form-group:has(input:focus) {
border-color: blue;
}
.form-group:has(input:valid) label {
color: green;
}

Benefits:

  • Pure CSS solution
  • No JavaScript required
  • Real-time state reflection
  • Cleaner markup
  • Better performance
/* Highlight form group when input is focused */
.form-group:has(:focus) {
background-color: #f0f9ff;
border-left: 3px solid #3b82f6;
}
/* Show helper text only when input is focused */
.helper-text {
display: none;
}
.form-group:has(:focus) .helper-text {
display: block;
}
/* Style labels based on input state */
.form-group:has(:required) label::after {
content: ' *';
color: red;
}
.form-group:has(:disabled) {
opacity: 0.5;
}
/* Different layout when card has an image */
.card:has(img) {
display: grid;
grid-template-columns: 200px 1fr;
}
.card:not(:has(img)) {
padding: 24px;
}
/* Feature card when it has a badge */
.card:has(.badge) {
border: 2px solid gold;
}
/* Style nav item containing current page */
nav li:has(a[aria-current='page']) {
background-color: #e0e7ff;
}
/* Dropdown parent styling */
.nav-item:has(.dropdown:hover) {
background-color: #f3f4f6;
}
/* Has submenu indicator */
.nav-item:has(ul)::after {
content: '';
margin-left: auto;
}
/* Grid adjusts based on content */
.grid:has(> :nth-child(4)) {
grid-template-columns: repeat(2, 1fr);
}
.grid:has(> :nth-child(7)) {
grid-template-columns: repeat(3, 1fr);
}
/* Empty state */
.list:not(:has(li)) {
display: none;
}
.list:not(:has(li)) + .empty-state {
display: block;
}
/* Style table row with checkbox checked */
tr:has(input[type='checkbox']:checked) {
background-color: #dbeafe;
}
/* Quantity selector */
.quantity:has(input:invalid) .error-message {
display: block;
}
/* Toggle visibility based on checkbox */
.settings:has(#advanced-mode:checked) .advanced-options {
display: block;
}
/* Style elements BEFORE a specific element */
/* (Previously impossible in CSS) */
/* All list items before the active one */
li:has(~ li.active) {
opacity: 1;
}
/* Progress bar - filled segments */
.step:has(~ .step.current),
.step.current {
background-color: #3b82f6;
}
/* Multiple conditions */
.form-group:has(:focus):has(:invalid) {
border-color: orange;
}
/* Negation */
section:not(:has(h2)) {
padding-top: 0;
}
/* With :is() for grouping */
article:has(:is(h1, h2, h3)) {
margin-top: 2em;
}
/* Quantity queries */
ul:has(li:nth-child(10)) {
columns: 2;
}

Supported in all modern browsers. See caniuse.com/css-has.

  • :has() can be expensive if used with complex selectors
  • Avoid deep descendant searches like :has(* .deeply .nested)
  • Keep selectors inside :has() simple
  • Browser engines are optimizing :has() performance continuously
  • Test performance with realistic DOM sizes

Use :has() when:

  • Styling parents based on child state
  • Detecting presence of specific descendants
  • Building form validation UI
  • Creating layout variations based on content
  • Implementing “previous sibling” selection

Avoid :has() when:

  • Simple descendant or sibling selectors suffice
  • The selector inside :has() is very complex
  • Targeting elements that change extremely frequently