Back to Blog
CSSSelectorshasParent Selector

CSS :has() Selector Guide

Master the CSS :has() parent selector. From form validation to card layouts to conditional styling.

B
Bootspring Team
Engineering
March 17, 2021
5 min read

The :has() selector styles elements based on their descendants. It's the long-awaited parent selector.

Basic Usage#

1/* Style parent based on child */ 2.card:has(img) { 3 /* Card contains an image */ 4 padding-top: 0; 5} 6 7.card:has(.featured) { 8 /* Card contains featured element */ 9 border: 2px solid gold; 10} 11 12/* Style based on child state */ 13.form-group:has(input:focus) { 14 /* Form group contains focused input */ 15 box-shadow: 0 0 0 2px blue; 16} 17 18.nav-item:has(a:hover) { 19 /* Nav item contains hovered link */ 20 background: rgba(0, 0, 0, 0.1); 21}

Form Validation Styles#

1/* Valid input */ 2.form-group:has(input:valid) { 3 border-color: green; 4} 5 6.form-group:has(input:valid) .icon-valid { 7 display: block; 8} 9 10/* Invalid input */ 11.form-group:has(input:invalid) { 12 border-color: red; 13} 14 15.form-group:has(input:invalid) .error-message { 16 display: block; 17} 18 19/* Required field indicator */ 20.form-group:has(input:required) label::after { 21 content: ' *'; 22 color: red; 23} 24 25/* Focused state */ 26.form-group:has(input:focus) { 27 outline: 2px solid blue; 28 outline-offset: 2px; 29} 30 31.form-group:has(input:focus) label { 32 color: blue; 33 font-weight: bold; 34}

Card Layouts#

1/* Card with image */ 2.card:has(> img:first-child) { 3 padding-top: 0; 4} 5 6.card:has(> img:first-child) .card-content { 7 padding-top: 1rem; 8} 9 10/* Card without image */ 11.card:not(:has(img)) { 12 background: linear-gradient(to bottom, #f0f0f0, white); 13} 14 15/* Featured cards */ 16.card:has(.badge-featured) { 17 border: 2px solid gold; 18 box-shadow: 0 4px 12px rgba(255, 215, 0, 0.3); 19} 20 21/* Cards with specific content */ 22.card:has(.price-sale) .price-original { 23 text-decoration: line-through; 24 opacity: 0.6; 25}
1/* Dropdown menus */ 2.nav-item:has(.dropdown) { 3 position: relative; 4} 5 6.nav-item:has(.dropdown)::after { 7 content: '▼'; 8 margin-left: 0.5rem; 9 font-size: 0.75em; 10} 11 12/* Active state based on current page */ 13.nav-item:has(a[aria-current="page"]) { 14 border-bottom: 2px solid currentColor; 15} 16 17/* Mega menu width */ 18nav:has(.mega-menu:hover) { 19 position: relative; 20}

Checkbox and Radio Styling#

1/* Style based on checked state */ 2.option:has(input:checked) { 3 background: #e0f0ff; 4 border-color: blue; 5} 6 7.option:has(input:checked) .icon { 8 color: blue; 9} 10 11/* Toggle switch */ 12.toggle:has(input:checked) .toggle-track { 13 background: green; 14} 15 16.toggle:has(input:checked) .toggle-thumb { 17 transform: translateX(100%); 18} 19 20/* Radio group - highlight selected */ 21.radio-group:has(input:checked) .radio-item:not(:has(input:checked)) { 22 opacity: 0.6; 23}

Conditional Layouts#

1/* Grid with sidebar */ 2.layout:has(.sidebar) { 3 display: grid; 4 grid-template-columns: 250px 1fr; 5} 6 7.layout:not(:has(.sidebar)) { 8 display: block; 9} 10 11/* Table with selection */ 12table:has(input:checked) { 13 /* Table has selected rows */ 14} 15 16table:has(input:checked) thead { 17 /* Show bulk actions */ 18} 19 20tr:has(input:checked) { 21 background: #f0f8ff; 22}

Sibling Styling#

1/* Style siblings of hovered element */ 2.grid:has(.item:hover) .item:not(:hover) { 3 opacity: 0.5; 4 filter: grayscale(0.5); 5} 6 7/* Gallery hover effect */ 8.gallery:has(img:hover) img:not(:hover) { 9 transform: scale(0.95); 10 opacity: 0.7; 11} 12 13/* Highlight row on hover */ 14tbody:has(tr:hover) tr:not(:hover) { 15 opacity: 0.7; 16}

Empty State Handling#

1/* Container has no items */ 2.list:not(:has(.list-item)) { 3 display: grid; 4 place-items: center; 5 min-height: 200px; 6} 7 8.list:not(:has(.list-item))::before { 9 content: 'No items found'; 10 color: #666; 11} 12 13/* Hide empty sections */ 14section:not(:has(*:not(h2))) { 15 display: none; 16}

Media Queries with :has()#

1/* Different layout based on content */ 2@container (min-width: 400px) { 3 .card:has(img) { 4 display: grid; 5 grid-template-columns: 150px 1fr; 6 } 7} 8 9/* Responsive behavior */ 10@media (max-width: 768px) { 11 nav:has(.menu-open) { 12 /* Mobile menu is open */ 13 position: fixed; 14 inset: 0; 15 background: white; 16 } 17}

Quantity Queries#

1/* Style based on number of children */ 2.grid:has(> *:nth-child(4)) { 3 /* Has at least 4 items */ 4 grid-template-columns: repeat(2, 1fr); 5} 6 7.grid:has(> *:nth-child(7)) { 8 /* Has at least 7 items */ 9 grid-template-columns: repeat(3, 1fr); 10} 11 12/* Single item */ 13.list:has(> *:only-child) { 14 text-align: center; 15} 16 17/* Few items */ 18.list:not(:has(> *:nth-child(3))) { 19 /* Less than 3 items */ 20 display: flex; 21 justify-content: center; 22}

State-Based Theming#

1/* Dark mode toggle */ 2html:has(#dark-mode:checked) { 3 color-scheme: dark; 4 --bg: #1a1a1a; 5 --text: #fff; 6} 7 8html:has(#dark-mode:checked) body { 9 background: var(--bg); 10 color: var(--text); 11} 12 13/* Compact mode */ 14html:has(#compact-mode:checked) { 15 --spacing: 0.5rem; 16 --font-size: 0.875rem; 17}
1/* Body when modal is open */ 2body:has(.modal[open]) { 3 overflow: hidden; 4} 5 6body:has(.modal[open])::before { 7 content: ''; 8 position: fixed; 9 inset: 0; 10 background: rgba(0, 0, 0, 0.5); 11 z-index: 999; 12} 13 14/* Page dimming */ 15main:has(~ .modal[open]) { 16 filter: blur(2px); 17 pointer-events: none; 18}

Combining with Other Selectors#

1/* Complex selections */ 2article:has(h2):has(img):has(p) { 3 /* Article with heading, image, and paragraph */ 4} 5 6/* :has with :is and :where */ 7.card:has(:is(h2, h3)) { 8 /* Card with h2 or h3 */ 9} 10 11/* Negation */ 12.card:has(img):not(:has(.badge)) { 13 /* Card with image but no badge */ 14} 15 16/* Adjacent sibling */ 17.item:has(+ .item:hover) { 18 /* Item before hovered item */ 19}

Performance Considerations#

1/* Avoid complex nested :has() */ 2/* Slow - deeply nested */ 3.a:has(.b:has(.c:has(.d))) { 4 /* Avoid this */ 5} 6 7/* Better - flatten */ 8.a:has(.d) { 9 /* Check for descendant directly */ 10} 11 12/* Limit scope when possible */ 13.card:has(> img) { 14 /* Direct child only */ 15} 16 17/* Use with specific selectors */ 18.form-group:has(input:invalid) { 19 /* Specific element type */ 20}

Browser Support Fallback#

1/* Feature detection */ 2@supports selector(:has(*)) { 3 .card:has(img) { 4 padding-top: 0; 5 } 6} 7 8/* Fallback for unsupported browsers */ 9@supports not selector(:has(*)) { 10 .card.has-image { 11 padding-top: 0; 12 } 13}

Best Practices#

Usage: ✓ Style parent based on child state ✓ Create conditional layouts ✓ Form validation feedback ✓ Interactive hover effects Performance: ✓ Use direct child selectors (>) ✓ Be specific with element types ✓ Avoid deeply nested :has() ✓ Test performance impact Accessibility: ✓ Ensure focus states are visible ✓ Don't hide important content ✓ Maintain keyboard navigation ✓ Test with screen readers

Conclusion#

The :has() selector enables parent styling based on descendants, something CSS couldn't do before. Use it for form validation, conditional layouts, and interactive effects. Be mindful of performance with complex selectors and provide fallbacks for older browsers.

Share this article

Help spread the word about Bootspring