:has() Selector
The :has() relational pseudo-class allows selecting elements based on their descendants, enabling “parent selectors” and complex relational styling that previously required JavaScript.
The Old Way
Section titled “The Old Way”<!-- Adding classes via JavaScript --><div class="form-group has-error"> <label>Email</label> <input type="email" class="error" /> <span class="error-message">Invalid email</span></div>// JavaScript to add parent classesdocument.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'); } });});/* Styling based on JavaScript-added class */.form-group.has-error label { color: red;}Problems:
- JavaScript required for parent-based styling
- Extra classes clutter the markup
- Event listeners add complexity
- State synchronization issues
- Performance overhead
The Modern Way
Section titled “The Modern Way”/* Style parent based on child state - no JavaScript! */.form-group:has(input:invalid) label { color: red;}
.form-group:has(input:focus) { border-color: blue;}
.form-group:has(input:valid) label { color: green;}Benefits:
- Pure CSS solution
- No JavaScript required
- Real-time state reflection
- Cleaner markup
- Better performance
Practical Examples
Section titled “Practical Examples”Form Styling
Section titled “Form Styling”/* Highlight form group when input is focused */.form-group:has(:focus) { background-color: #f0f9ff; border-left: 3px solid #3b82f6;}
/* Show helper text only when input is focused */.helper-text { display: none;}.form-group:has(:focus) .helper-text { display: block;}
/* Style labels based on input state */.form-group:has(:required) label::after { content: ' *'; color: red;}
.form-group:has(:disabled) { opacity: 0.5;}Card with Image Detection
Section titled “Card with Image Detection”/* Different layout when card has an image */.card:has(img) { display: grid; grid-template-columns: 200px 1fr;}
.card:not(:has(img)) { padding: 24px;}
/* Feature card when it has a badge */.card:has(.badge) { border: 2px solid gold;}Navigation States
Section titled “Navigation States”/* Style nav item containing current page */nav li:has(a[aria-current='page']) { background-color: #e0e7ff;}
/* Dropdown parent styling */.nav-item:has(.dropdown:hover) { background-color: #f3f4f6;}
/* Has submenu indicator */.nav-item:has(ul)::after { content: '▼'; margin-left: auto;}Responsive Container Adjustments
Section titled “Responsive Container Adjustments”/* Grid adjusts based on content */.grid:has(> :nth-child(4)) { grid-template-columns: repeat(2, 1fr);}
.grid:has(> :nth-child(7)) { grid-template-columns: repeat(3, 1fr);}
/* Empty state */.list:not(:has(li)) { display: none;}
.list:not(:has(li)) + .empty-state { display: block;}Interactive Elements
Section titled “Interactive Elements”/* Style table row with checkbox checked */tr:has(input[type='checkbox']:checked) { background-color: #dbeafe;}
/* Quantity selector */.quantity:has(input:invalid) .error-message { display: block;}
/* Toggle visibility based on checkbox */.settings:has(#advanced-mode:checked) .advanced-options { display: block;}Previous Sibling Selection
Section titled “Previous Sibling Selection”/* Style elements BEFORE a specific element *//* (Previously impossible in CSS) */
/* All list items before the active one */li:has(~ li.active) { opacity: 1;}
/* Progress bar - filled segments */.step:has(~ .step.current),.step.current { background-color: #3b82f6;}Combining with Other Selectors
Section titled “Combining with Other Selectors”/* Multiple conditions */.form-group:has(:focus):has(:invalid) { border-color: orange;}
/* Negation */section:not(:has(h2)) { padding-top: 0;}
/* With :is() for grouping */article:has(:is(h1, h2, h3)) { margin-top: 2em;}
/* Quantity queries */ul:has(li:nth-child(10)) { columns: 2;}Browser Support
Section titled “Browser Support”Supported in all modern browsers. See caniuse.com/css-has.
Performance Considerations
Section titled “Performance Considerations”:has()can be expensive if used with complex selectors- Avoid deep descendant searches like
:has(* .deeply .nested) - Keep selectors inside
:has()simple - Browser engines are optimizing
:has()performance continuously - Test performance with realistic DOM sizes
When to Use
Section titled “When to Use”Use :has() when:
- Styling parents based on child state
- Detecting presence of specific descendants
- Building form validation UI
- Creating layout variations based on content
- Implementing “previous sibling” selection
Avoid :has() when:
- Simple descendant or sibling selectors suffice
- The selector inside
:has()is very complex - Targeting elements that change extremely frequently