CSS :has() Selector
debt(d5/e3/b3/t5)
Closest to 'specialist tool catches' (d5). Broad :has() selectors causing performance issues require DevTools performance profiler to detect. Browser compatibility issues (Firefox <121) can be caught by caniuse-based linters or build tools, but performance problems from overly general selectors only surface during performance profiling or runtime testing.
Closest to 'simple parameterised fix' (e3). The quick_fix shows replacing JS class toggles with :has() is localized refactoring. Fixing performance issues means making selectors more specific (.card:has(.badge) instead of div:has(span)), which is a pattern replacement within CSS files but may touch multiple selectors across stylesheets.
Closest to 'localised tax' (b3). :has() is a CSS selector feature that affects styling code only. Poor usage (broad selectors) creates a localized performance tax in rendering, but doesn't spread architectural weight across the system. The choice impacts CSS maintainers but doesn't shape application architecture or require other components to adapt.
Closest to 'notable trap' (t5). The misconception that ':has() is slow in all cases' is a documented gotcha that developers eventually learn. The reality is nuanced—it's performant with specific class selectors but expensive with broad element selectors. This contradicts the simplistic mental model that all CSS selectors have similar performance characteristics, creating a notable but learnable trap.
Also Known As
TL;DR
Explanation
:has() accepts a relative selector list and matches elements for which the relative selector matches. It can look forward (parent contains child), sideways (h1:has(+ p) — h1 followed by p), and combine with other pseudo-classes. :has() has been called the 'holy grail' of CSS selectors because it closes the gap between CSS and JavaScript-based conditional styling. Supported in Chrome 105+, Safari 15.4+, Firefox 121+. Performance: browsers evaluate :has() bottom-up, which can be expensive for broad selectors; specificity is calculated including the argument's specificity.
Common Misconception
Why It Matters
Common Mistakes
- Using broad :has() selectors — div:has(span) matches every div containing any span; be specific with classes to avoid performance issues.
- Expecting :has() to work in Firefox before 121 — Firefox added :has() in December 2023; check your support requirements.
- Nesting :has() inside :has() — doubly nested relational pseudo-classes are not well supported; keep :has() at the top level.
- Replacing all JavaScript conditionals with :has() — :has() is CSS; it cannot trigger callbacks or update data. Use it for visual state only, not business logic.
Code Examples
/* ❌ JavaScript to add parent class when child state changes */
/* CSS */
.form-group.has-error { border-left: 3px solid red; }
.card.has-image { padding: 0; }
/* JavaScript — required just for parent styling */
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');
});
});
/* ✅ :has() — no JavaScript needed for parent styling */
/* Highlight the form group when its input is invalid */
.form-group:has(input:invalid) {
border-left: 3px solid red;
}
/* Remove card padding when it contains an image */
.card:has(> img) {
padding: 0;
}
/* Style a nav item when its dropdown is open */
.nav-item:has(.dropdown[open]) {
background: rgba(0,0,0,.05);
}
/* h2 followed immediately by p — adjacent sibling */
h2:has(+ p) {
margin-bottom: 4px; /* Less space when followed by text */
}
/* Dark mode toggle without JS */
body:has(#dark-mode:checked) {
background: #1a1a1a;
color: #e0e0e0;
}