Skip to main content
โšก Calmops

CSS :has() Selector: The Parent Selector Revolution

The CSS :has() selector is one of the most powerful and revolutionary additions to CSS in recent years. Often called the “parent selector,” :has() allows you to select elements based on their descendants, fundamentally changing how we write CSS.

What is the :has() Selector?

The :has() pseudo-class represents an element if any of the selectors passed as arguments match. It was historically called the “parent selector” because it can select a parent based on its children.

/* Select all cards that contain an image */
.card:has(img) {
  border: 2px solid blue;
}

/* Select forms with invalid inputs */
form:has(input:invalid) {
  background-color: #fee;
}

Basic Syntax

/* Select element if it has matching descendants */
parent:has(selector) { }

/* Select element if it contains specific children */
.container:has(.child) { }

/* Multiple conditions - element must have BOTH */
article:has(h1):has(.featured-image) { }

/* Any of conditions - element has AT LEAST ONE */
article:has(h1, h2, h3) { }

The Problem: Why We Needed :has()

Before :has(), styling parents based on children was impossible without JavaScript. Consider a navigation menu where you want to highlight a parent item when any of its dropdown items are active.

The Old Approach: JavaScript

// Without :has(), we needed JavaScript
document.querySelectorAll('.menu-item').forEach(item => {
  if (item.querySelector('.active')) {
    item.classList.add('has-active-child');
  }
});
/* Then style based on class added by JS */
.menu-item.has-active-child > a {
  font-weight: bold;
  color: blue;
}

The Modern Approach: Pure CSS

/* With :has(), it's simple */
.menu-item:has(.active) > a {
  font-weight: bold;
  color: blue;
}

Practical Use Cases

1. Form Validation Styling

Style a form differently when it contains invalid input:

/* Highlight form when any input is invalid */
form:has(input:invalid) .submit-btn {
  opacity: 0.5;
  pointer-events: none;
}

/* Show validation message only when input is invalid */
.form-group:has(input:invalid) .error-message {
  display: block;
}

/* Style invalid inputs differently within invalid forms */
form:has(input:invalid) input:invalid {
  border-color: red;
  background-color: #fff5f5;
}
<form>
  <div class="form-group">
    <label>Email</label>
    <input type="email" required placeholder="[email protected]">
    <span class="error-message">Please enter a valid email</span>
  </div>
  <button class="submit-btn">Submit</button>
</form>

2. Empty State Handling

Style containers differently when they have no content:

/* Hide empty containers */
.container:empty {
  display: none;
}

/* Show placeholder for empty containers */
.card-container:empty::after {
  content: "No items to display";
  display: block;
  padding: 20px;
  text-align: center;
  color: #666;
}

/* Style cards without images differently */
.product-card:not(:has(img)) {
  padding-left: 1rem;
}

3. Navigation Active States

Automatically highlight navigation items with active dropdowns:

/* Highlight parent when child is focused */
nav a:has(+ .dropdown:focus) {
  background-color: #e3f2fd;
}

/* Highlight parent when any descendant is active */
.nav-item:has(.nav-link.active) {
  border-left: 3px solid blue;
}

/* Style menu when any submenu is open */
.menu:has(.submenu:hover) {
  z-index: 100;
}

4. Card Variations

Create different card styles based on content:

/* Featured card - has both image and badge */
.card:has(img):has(.badge) {
  position: relative;
}

/* Card with video */
.card:has(video) {
  background-color: #f0f0f0;
}

/* Card without description */
.card:not(:has(p)) {
  min-height: 200px;
}
<!-- Different styling based on content -->
<article class="card">
  <img src="hero.jpg">
  <span class="badge">New</span>
  <h3>Featured Article</h3>
  <p>Description here</p>
</article>

5. Table Row States

Style table rows based on content or state:

/* Highlight rows with important items */
tr:has(.priority-high) {
  background-color: #fff3cd;
}

/* Show action column only when needed */
table:has(.action-trigger) .actions-column {
  display: table-cell;
}

/* Style rows with checked checkboxes */
tr:has(input:checked) {
  background-color: #e8f5e9;
}

Combining :has() with Other Selectors

With Pseudo-classes

/* Select card that has an image that's being hovered */
.card:has(img:hover) {
  transform: scale(1.02);
}

/* Form with focused input */
form:has(input:focus) {
  box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.3);
}

/* Checkbox that is checked */
label:has(input:checked) {
  font-weight: bold;
  color: #2b6cb0;
}

With Pseudo-elements

/* Add indicator when card has a link */
.card:has(a)::after {
  content: "โ†’";
  float: right;
}

/* Show badge when item is new */
.item:has(.new-badge)::before {
  content: "NEW";
  font-size: 0.7em;
  background: red;
  color: white;
  padding: 2px 6px;
  border-radius: 3px;
  margin-right: 8px;
}

With Combinators

/* Direct child has specific class */
container:has(> .featured) {
  grid-template-columns: 1fr 300px;
}

/* Adjacent sibling */
h2:has(+ p) {
  margin-bottom: 0;
}

/* General sibling - highlight section with any code blocks */
section:has(~ pre) {
  background: #f7fafc;
  padding: 1rem;
}

Complex Combinations

/* Article with both featured image and author */
article:has(.featured-image):has(.author) {
  grid-template-areas: 
    "image title"
    "image author";
}

/* Form with multiple invalid inputs */
form:has(input:invalid):has(input:focus) {
  animation: shake 0.3s;
}

/* Navigation with multiple active items */
nav:has(.active):has(.active:hover) {
  background: linear-gradient(to bottom, #e0e0e0, #c0c0c0);
}

Performance Considerations

selector:has() Performance

The :has() selector can be performance-intensive because it needs to check descendants. Here are best practices:

Do: Use Specific Selectors

/* Good: Specific selector */
.card:has(.specific-image) { }

/* Bad: Too broad */
.container:has(*) { }

Do: Limit Scope

/* Better: Limit to specific containers */
nav:has(.active) { }
sidebar:has(.widget) { }

/* Avoid: Too general */
body:has(.anything) { }

Consider: CSS Containment

/* Use containment for better performance */
.card-container {
  contain: content;
}

.card-container:has(.expensive-element) {
  /* Styles */
}

Avoid: Deep Nesting

/* Bad for performance */
div:has(span:has(a:has(img))) { }

/* Better */
.card:has(.thumbnail) { }

Browser Support

The :has() selector has excellent modern browser support:

/* Feature detection */
@supports (selector(:has(*))) {
  /* :has() is supported */
  .card:has(img) { }
}
Browser Version Release Date
Chrome 105+ Aug 2022
Safari 15.4+ Mar 2022
Firefox 121+ Dec 2023
Edge 105+ Aug 2022

Real-World Examples

Example 1: Responsive Card Grid

/* Default: 3 columns */
.card-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}

/* When any card has video, switch to 2 columns */
.card-grid:has(.card video) {
  grid-template-columns: repeat(2, 1fr);
}

/* When card with badge exists, add special styling */
.card-grid:has(.badge) .card:first-child {
  border: 2px solid gold;
}

Example 2: Smart Form Validation

/* Base styles */
.form-field {
  margin-bottom: 1rem;
}

.error-message {
  display: none;
  color: red;
  font-size: 0.875rem;
}

/* Show error when input is invalid */
.form-field:has(input:invalid) .error-message {
  display: block;
}

/* Style field when invalid */
.form-field:has(input:invalid) input {
  border-color: red;
}

/* When form has any error, disable submit */
form:has(input:invalid) button[type="submit"] {
  opacity: 0.6;
  cursor: not-allowed;
}

Example 3: Adaptive Navigation

/* Default horizontal nav */
.main-nav {
  display: flex;
  gap: 1rem;
}

/* When nav has dropdowns, add arrow indicator */
.main-nav li:has(ul) > a::after {
  content: " โ–ผ";
  font-size: 0.7em;
}

/* When on mobile (nav has hamburger) */
.main-nav:has(.hamburger) {
  flex-direction: column;
}

Example 4: Shopping Cart Badge

/* Hide badge when cart is empty */
.cart-icon:has(.cart-count:empty) {
  display: none;
}

/* Show badge when items exist */
.cart-icon:has(.cart-count:not(:empty)) .cart-badge {
  display: flex;
}

/* Highlight cart when it has expensive items */
.cart:has(.item-price[data-high="true"]) {
  border-color: gold;
}

Common Patterns

Pattern 1: Has Class

/* Select element if it has a specific class */
.element:has(.class) { }

/* Equivalent to */
.element.class { }

Pattern 2: Has Attribute

/* Select input that has a placeholder */
input:has([placeholder]) { }

/* Select form that has required fields */
form:has([required]) { }

Pattern 3: Negation with :not()

/* Select card without image */
.card:not(:has(img)) { }

/* Select form without errors */
form:not(:has(.error)) { }

Pattern 4: Multiple Conditions

/* AND - must have both */
.card:has(img):has(.badge) { }

/* OR - has at least one */
.card:has(img, .badge) { }

Debugging :has() Selectors

Using DevTools

In Chrome DevTools:

  1. Inspect element
  2. Look at Styles panel
  3. See computed styles from :has() rules

Common Issues

/* Issue: :has() not matching */
/* Check: Is the selector specific enough? */

/* Issue: Performance problems */
/* Fix: Add containment, limit scope */

/* Issue: Not working in some browsers */
/* Fix: Use @supports for progressive enhancement */

Progressive Enhancement

Use @supports to provide fallbacks:

/* Base styles - works everywhere */
.card {
  padding: 1rem;
}

/* Enhanced styles with :has() */
@supports (selector(:has(*))) {
  .card:has(img) {
    padding-left: 200px;
  }
  
  .card:has(img) img {
    position: absolute;
    left: 1rem;
  }
}

External Resources

Summary

The CSS :has() selector revolutionizes how we write CSS by enabling parent selection based on descendants. Key takeaways:

  • Parent Selection: Style parents based on children without JavaScript
  • Form Validation: Create sophisticated form states with pure CSS
  • Empty States: Handle empty containers elegantly
  • Navigation: Build adaptive navigation systems
  • Performance: Use specific selectors and limit scope
  • Browser Support: Excellent support in modern browsers (Chrome 105+, Safari 15.4+, Firefox 121+)

Start using :has() today to write more maintainable and expressive CSS!

Comments