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:
- Inspect element
- Look at Styles panel
- 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
- MDN: :has() pseudo-class
- CSS Working Group: :has() specification
- Can I Use: :has()
- Web.dev: CSS :has()
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