CSS :has() Advanced Patterns
Beyond basic parent selection with :has(). Learn multi-level selection, performance optimization, browser fallbacks, and dynamic form validation patterns.
Beyond basic parent selection, :has() enables complex relational patterns previously impossible in pure CSS. Multi-level parent selection, conditional styling based on sibling states, and dynamic form validation all become possible without JavaScript.
The Problem: Beyond Basic :has()
Most :has() examples show simple parent selection (div:has(.child)). But real-world styling requires understanding:
- Multi-level parent selection
- Performance implications of complex selectors
- Browser fallback strategies
- Dynamic form validation states
Pattern 1: Multi-Level Parent Selection
Select grandparents or ancestors based on deep descendants:
/* Select article if it contains a heading with a link */
article:has(h2 a) {
/* Article has a linked heading */
}
/* Select card if it contains an image AND a button */
.card:has(img):has(button) {
/* Card is actionable content */
}
/* Select section if ANY descendant has error class */
section:has(.error) {
border-left: 3px solid red;
}
Chain :has() for multiple conditions:
/* Select grid if it has more than 4 items AND has an active item */
.grid:has(> :nth-child(5)):has(.active) {
/* Grid with 5+ items, one of which is active */
}Pattern 2: Conditional Sibling Selection
Style elements based on siblings' children:
/* Style label based on its input's validity */
label:has(+ input:invalid) {
color: red;
}
/* Style button based on previous sibling's state */
button:has(+ .processing) {
opacity: 0.5;
pointer-events: none;
}
/* Style div based on following sibling having a class */
.container:has(~ .success) {
border-color: green;
}Pattern 3: Dynamic Form Validation
Create rich form validation states without JavaScript:
/* Fieldset with any invalid field */
fieldset:has(:invalid) {
border-color: var(--error);
background: var(--error-bg);
}
/* Fieldset when all fields are valid */
fieldset:has(:valid):not(:has(:invalid)) {
border-color: var(--success);
}
/* Submit button state */
form:has(:invalid) button[type="submit"] {
opacity: 0.5;
cursor: not-allowed;
}
form:not(:has(:invalid)) button[type="submit"] {
opacity: 1;
cursor: pointer;
}
Combine with :focus-within:
/* Highlight form row when focused */
.form-row:has(:focus-within) {
background: var(--highlight);
}
/* Show helper text only when input is invalid */
.form-row:has(input:invalid) .helper {
display: block;
color: var(--error);
}
/* Hide helper when input is valid */
.form-row:has(input:valid) .helper {
display: none;
}Pattern 4: Quantity-Based Styling
Change layouts based on child count:
/* Grid changes columns based on item count */
.grid:has(> :nth-child(1)) {
grid-template-columns: 1fr;
}
.grid:has(> :nth-child(2)) {
grid-template-columns: 1fr 1fr;
}
.grid:has(> :nth-child(4)) {
grid-template-columns: repeat(2, 1fr);
}
.grid:has(> :nth-child(6)) {
grid-template-columns: repeat(3, 1fr);
}
/* Full width for single item */
.grid:not(:has(> :nth-child(2))) > * {
grid-column: 1 / -1;
}Pattern 5: State-Based Styling
React to complex component states:
/* Card when its image is hovered */
.card:has(img:hover) {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
}
/* Dropdown menu when parent li is hovered */
nav li:has(.dropdown:hover) {
background: var(--nav-active);
}
/* Tooltip when target is focused */
button:has([aria-label]:focus-visible) {
position: relative;
}
button:has([aria-label]:focus-visible)::after {
content: attr(aria-label);
position: absolute;
bottom: 100%;
background: var(--tooltip-bg);
padding: 0.5rem;
}Edge Case 1: Performance Impact
:has() can be expensive on complex selectors:
/* ❌ Bad: Expensive selector */
*:has(.active) { }
/* This checks every single element on the page */
/* ✅ Good: Scoped selector */
.container:has(.active) { }
/* Only checks elements within .container */
/* ✅ Better: Direct child check */
.container:has(> .active) { }
/* Only checks direct children, not all descendants */
Profile :has() performance:
/* Chrome DevTools > More Tools > Rendering */
/* Enable "Selector Stats" to see expensive selectors */
/* Common performance issues:
- Universal selectors with :has()
- Deep descendant searches
- Complex combinators inside :has() */Edge Case 2: Browser Fallbacks
:has() is unsupported in older browsers. Use progressive enhancement:
/* Base styles (works everywhere) */
.card {
border: 1px solid var(--border);
}
/* Progressive enhancement with :has() */
@supports selector(:has(*)) {
.card:has(.featured) {
border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent);
}
}
/* Alternative: JavaScript fallback */
.no-has-support .card.featured {
border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent);
}Feature detection in JavaScript:
// Detect :has() support
function supportsHas() {
return CSS.supports('selector(:has(*))');
}
if (!supportsHas()) {
document.documentElement.classList.add('no-has-support');
// Add :has() polyfill or apply fallback styles
}Edge Case 3: Specificity Calculation
:has() contributes to specificity based on its most specific argument:
/* Specificity: (0, 1, 1) - element + class */
div:has(.foo) { }
/* Specificity: (1, 0, 1) - element + ID */
div:has(#bar) { }
/* Specificity: (0, 2, 0) - two classes */
:has(.foo.bar) { }
/* Specificity: (0, 2, 1) - element + two classes in :has() */
div:has(.foo.bar) { }Specificity examples:
/* Lower specificity */
.card:has(img) { }
/* Higher specificity (due to ID in :has()) */
.card:has(#featured-image) { }
/* This wins even though selector looks similar */
.card:has(#featured-image) {
border: 2px solid gold;
}Pattern 6: Combinator Chaining
Use combinators inside :has() for complex relationships:
/* Select div if it has a p followed by a button */
div:has(p + button) { }
/* Select section if it has h2 followed by paragraph */
section:has(h2 + p) { }
/* Select list if it has li with class .active */
ul:has(li.active) { }
/* Select grid if it has div with class .card as child */
.grid:has(> .card) { }Pattern 7: Empty State Handling
Show empty states based on content:
/* List has no visible items */
.list:not(:has(li:not([hidden]))) {
display: flex;
justify-content: center;
min-height: 200px;
}
.list:not(:has(li:not([hidden])))::after {
content: "No items to display";
color: var(--muted);
}
/* List has items */
.list:has(li:not([hidden]))::after {
content: none;
}
/* Show empty state only when list is empty */
.empty-state {
display: none;
}
.list:not(:has(li)) .empty-state {
display: flex;
}Pattern 8: Responsive Design Without Media Queries
Use :has() for container-based responsiveness:
/* Card layout changes based on content */
.card:has(img) {
display: grid;
grid-template-columns: 200px 1fr;
gap: 1rem;
}
.card:not(:has(img)) {
display: block;
padding: 1.5rem;
}
/* Navigation changes based on presence of dropdown */
nav:has(.dropdown) {
position: relative;
}
nav:not(:has(.dropdown)) {
position: static;
}Real-World Example: Complex Form
<form>
<div class="form-row">
<label>Email</label>
<input type="email" required />
<span class="helper">We'll never spam you</span>
</div>
<div class="form-row">
<label>Password</label>
<input type="password" required minlength="8" />
<span class="helper">Minimum 8 characters</span>
</div>
<button type="submit">Sign Up</button>
</form>/* Form validation with :has() */
.form-row:has(:focus-within) {
background: var(--highlight);
}
.form-row:has(input:invalid) label {
color: var(--error);
}
.form-row:has(input:valid) label {
color: var(--success);
}
/* Show helper only when input is invalid */
.form-row:has(input:invalid) .helper {
display: block;
color: var(--error);
}
.form-row:has(input:valid) .helper {
display: none;
}
/* Submit button state */
form:has(:invalid) button[type="submit"] {
opacity: 0.5;
cursor: not-allowed;
background: var(--muted);
}
form:not(:has(:invalid)) button[type="submit"] {
opacity: 1;
cursor: pointer;
background: var(--primary);
}Key Takeaways
- Multi-level selection: Select ancestors based on deep descendants
- Performance: Scope selectors, use direct child checks (
>) - Progressive enhancement: Use
@supports selector(:has(*)) - Specificity: :has() contributes based on most specific argument
- Form validation: Create rich states without JavaScript
- Quantity queries: Style based on child count
- Combinators: Use
+,~,>inside :has()
Advertisement
Explore these curated resources to deepen your understanding
Official Documentation
Tools & Utilities
Related Insights
Explore related edge cases and patterns
Advertisement