CSS :has() Selector in Practice: The Parent Selector That Changes Component Styling Architecture
The :has() Selector: CSS's "Parent Selector"
For decades, CSS could only select from parent to child. :has() breaks that limitation, enabling child-to-parent reverse selection:
/* Traditional: only select descendants */
.card .title { color: blue; }
/* :has(): select parent based on child state */
.card:has(.title) { border-color: blue; }
Browser Support
| Browser | Supported Since | Release |
|---|---|---|
| Chrome | 105+ | 2022.08 |
| Safari | 15.4+ | 2022.03 |
| Firefox | 121+ | 2023.12 |
| Edge | 105+ | 2022.08 |
As of 2026, all major browsers support
:has().
Syntax and Matching Logic
Basic Syntax
/* Contains specified descendant */
parent:has(descendant) { }
/* Contains direct child */
parent:has(> child) { }
/* Sibling element exists */
element:has(+ sibling) { }
element:has(~ sibling) { }
/* Multiple conditions */
element:has(.a, .b) { } /* OR */
element:has(.a):has(.b) { } /* AND */
Combining with :not()
/* .nav without an .active child */
.nav:not(:has(.active)) {
opacity: 0.5;
}
/* Contains an unchecked checkbox */
.form:has(input:not(:checked)) {
border-color: orange;
}
Practice: Form Validation States
Traditional JS Approach
// Listen to each input's validation state
input.addEventListener('invalid', () => {
form.classList.add('has-error');
});
input.addEventListener('valid', () => {
form.classList.remove('has-error');
});
:has() Approach
/* Form with invalid inputs changes overall style */
form:has(:user-invalid) {
border-color: #ef4444;
}
form:has(:user-invalid) .submit-btn {
opacity: 0.5;
pointer-events: none;
}
/* Individual invalid field highlights its container */
.field:has(:user-invalid) {
background: #fef2f2;
}
.field:has(:user-invalid) .error-msg {
display: block;
}
/* All valid: show success message */
form:has(.field:not(:has(:user-invalid))) .success-msg {
display: block;
}
<form>
<div class="field">
<label>Email</label>
<input type="email" required />
<span class="error-msg">Please enter a valid email</span>
</div>
<div class="field">
<label>Password</label>
<input type="password" required minlength="8" />
<span class="error-msg">Password must be at least 8 characters</span>
</div>
<button class="submit-btn">Submit</button>
<span class="success-msg">Form is valid</span>
</form>
Practice: Card Layouts
Adaptive Cards With and Without Images
/* Cards with images: horizontal layout */
.card:has(img) {
display: flex;
gap: 1rem;
}
.card:has(img) .card-body {
flex: 1;
}
/* Cards without images: vertical layout */
.card:not(:has(img)) {
display: block;
text-align: center;
}
/* Landscape image: adjust ratio */
.card:has(img[width="1200"]) {
flex-direction: column;
}
Card Hover Effects
/* Hover anywhere on card: image scales */
.card:has(.card-link:hover) img {
transform: scale(1.05);
}
.card:has(.card-link:hover) .card-title {
color: var(--primary);
}
Practice: Navigation Highlighting
Traditional: Server-side active class
<nav>
<a href="/" class="active">Home</a>
<a href="/blog">Blog</a>
</nav>
:has() Approach: Pure CSS Page Awareness
/* Using aria-current attribute */
nav:has([aria-current="page"]) {
border-bottom-color: var(--primary);
}
nav a:has(+ [aria-current="page"]),
nav a[aria-current="page"] {
color: var(--primary);
font-weight: 600;
}
/* Show back button only when nav has active item */
nav:not(:has([aria-current="page"])) .back-btn {
display: none;
}
Sidebar Collapse Awareness
/* Sidebar with open submenu: show divider */
.sidebar:has(.submenu:not([hidden])) {
border-right: 2px solid var(--border);
}
/* All submenus collapsed: compact mode */
.sidebar:not(:has(.submenu:not([hidden]))) {
width: 64px;
}
Practice: Dark Mode Coordination
Auto-adjust Based on Image Brightness
/* Bright image: darken surrounding text */
figure:has(img.light) {
background: #1a1a1a;
color: #fff;
}
/* Dark mode: add background behind transparent logo */
@media (prefers-color-scheme: dark) {
.container:has(img[alt*="logo"]) {
background: #fff;
padding: 8px;
border-radius: 8px;
}
}
Practice: Table and List Interactions
Table Select-All Awareness
/* All checkboxes checked: header style changes */
table:has(tbody input:checked:not(:indeterminate):not(:only-child))
th {
background: var(--primary-light);
}
/* Some checked: show batch actions */
table:has(tbody input:checked) .batch-actions {
display: flex;
}
/* None checked: hide */
table:not(:has(tbody input:checked)) .batch-actions {
display: none;
}
List Empty State
/* Empty list shows placeholder */
.list:has(> :only-child.empty-placeholder) {
justify-content: center;
min-height: 200px;
}
:has() vs JavaScript: When to Use What
| Scenario | :has() Advantage | JS Advantage |
|---|---|---|
| DOM structure changes | Auto-responds, no listeners | Handles async data |
| Performance | Native, fast style recalc | Fine-grained control of timing |
| Complex logic | Simple conditions | Multi-step computation, API calls |
| Compatibility | Full support since 2023 | No compatibility concerns |
Performance Considerations
/* Bad: deeply nested :has() can hurt performance */
.container:has(.deep1:has(.deep2:has(.deep3))) { }
/* Good: flat :has() conditions */
.container:has(.deep3) { }
Avoid deeply nested
:has(). The browser must traverse the DOM tree to match — deeper nesting means more overhead.
Combining with Other CSS4 Features
:is() and :where() for Simpler Selectors
/* :is() simplifies multi-type checks */
.card:has(:is(img, video, svg)) {
aspect-ratio: 16/9;
}
/* :where() lowers specificity for easy overrides */
.card:where(:has(.featured)) {
border-color: gold;
}
:modal and :popover
/* Open modal: prevent background scrolling */
body:has(:modal) {
overflow: hidden;
}
body:has([popover]:not(:popover-closed)) {
overflow: hidden;
}
Best Practices
- Prefer :has() over JS class toggling: Less JS, co-located style logic
- Keep :has() conditions flat: Avoid deep nesting for performance
- Pair with semantic attributes:
aria-current,:user-invalid,:checked - Progressive enhancement: Core functionality shouldn't depend on :has()
- Debug with DevTools: Chrome DevTools supports :has() selector highlighting
Summary
The :has() selector is one of the most significant CSS capability expansions in history, giving CSS child-to-parent reverse selection and drastically reducing JavaScript for form validation, conditional styling, and state coordination. Understanding :has() matching logic and performance boundaries is essential for modern frontend engineers.
Use the Flexbox tool to practice layout combinations, the Box Shadow Generator for card shadow effects, and the Border Radius tool for card corner design.
Try these browser-local tools — no sign-up required →