Back to Blog
CSS:has()SelectorsParent Selector

CSS :has() Selector Guide

Master the CSS :has() parent selector for styling elements based on their descendants and siblings.

B
Bootspring Team
Engineering
April 1, 2019
5 min read

The :has() selector enables styling parent elements based on their children, finally bringing the "parent selector" to CSS. Here's how to use it.

Basic Syntax#

1/* Style parent if it contains a specific child */ 2.card:has(img) { 3 display: grid; 4 grid-template-rows: auto 1fr; 5} 6 7/* Style parent if it contains multiple elements */ 8.form-group:has(input:invalid) { 9 border-color: red; 10} 11 12/* Combine with other selectors */ 13article:has(> h2) { 14 padding-top: 2rem; 15}

Form Validation#

1/* Highlight form group with invalid input */ 2.form-group:has(input:invalid) { 3 background-color: #fef2f2; 4 border-left: 3px solid #ef4444; 5} 6 7/* Style label when input is focused */ 8.form-group:has(input:focus) label { 9 color: #3b82f6; 10 font-weight: 600; 11} 12 13/* Show error message only when invalid */ 14.form-group:has(input:invalid) .error-message { 15 display: block; 16} 17 18.form-group .error-message { 19 display: none; 20} 21 22/* Required field indicator */ 23.form-group:has(input:required) label::after { 24 content: ' *'; 25 color: #ef4444; 26}

Card Layouts#

1/* Card with image gets different layout */ 2.card:has(img) { 3 display: grid; 4 grid-template-rows: 200px 1fr; 5} 6 7.card:not(:has(img)) { 8 padding: 2rem; 9} 10 11/* Card with video gets aspect ratio */ 12.card:has(video) { 13 aspect-ratio: 16 / 9; 14} 15 16/* Card with multiple buttons */ 17.card:has(.btn + .btn) .card-footer { 18 display: flex; 19 gap: 1rem; 20 justify-content: flex-end; 21}
1/* Highlight nav item with active link */ 2.nav-item:has(.active) { 3 background-color: #f3f4f6; 4 border-radius: 8px; 5} 6 7/* Dropdown parent styling */ 8.nav-item:has(.dropdown:hover) { 9 position: relative; 10} 11 12.nav-item:has(.dropdown:hover)::after { 13 content: ''; 14 position: absolute; 15 bottom: -10px; 16 left: 0; 17 right: 0; 18 height: 10px; 19} 20 21/* Mobile menu indicator */ 22nav:has(.menu-open) { 23 background: white; 24 position: fixed; 25 inset: 0; 26}

Table Styling#

1/* Row with checkbox checked */ 2tr:has(input[type="checkbox"]:checked) { 3 background-color: #eff6ff; 4} 5 6/* Table with sortable columns */ 7table:has(th[data-sortable]) th { 8 cursor: pointer; 9} 10 11/* Empty table state */ 12table:has(tbody:empty)::after { 13 content: 'No data available'; 14 display: block; 15 padding: 2rem; 16 text-align: center; 17 color: #6b7280; 18} 19 20/* Table with selected rows */ 21table:has(tr.selected) .bulk-actions { 22 display: flex; 23}

Sibling Selection#

1/* Style element based on next sibling */ 2h2:has(+ p) { 3 margin-bottom: 0.5rem; 4} 5 6h2:has(+ ul) { 7 margin-bottom: 1rem; 8} 9 10/* Image followed by caption */ 11figure:has(img + figcaption) img { 12 border-radius: 8px 8px 0 0; 13} 14 15figure:has(img + figcaption) figcaption { 16 padding: 1rem; 17 background: #f3f4f6; 18 border-radius: 0 0 8px 8px; 19}

Quantity Queries#

1/* Container with 1 item */ 2.grid:has(> :only-child) { 3 justify-content: center; 4} 5 6/* Container with 2+ items */ 7.grid:has(> :nth-child(2)) { 8 grid-template-columns: repeat(2, 1fr); 9} 10 11/* Container with 3+ items */ 12.grid:has(> :nth-child(3)) { 13 grid-template-columns: repeat(3, 1fr); 14} 15 16/* Container with 4+ items */ 17.grid:has(> :nth-child(4)) { 18 grid-template-columns: repeat(4, 1fr); 19}

Dark Mode Detection#

1/* Style based on color scheme preference */ 2html:has(input#dark-mode:checked) { 3 --bg-color: #1f2937; 4 --text-color: #f9fafb; 5} 6 7html:has(input#dark-mode:checked) body { 8 background-color: var(--bg-color); 9 color: var(--text-color); 10} 11 12/* Toggle switch styling */ 13.theme-toggle:has(input:checked) .toggle-thumb { 14 transform: translateX(24px); 15}
1/* Body when modal is open */ 2body:has(.modal.open) { 3 overflow: hidden; 4} 5 6/* Backdrop when modal exists */ 7body:has(.modal.open)::before { 8 content: ''; 9 position: fixed; 10 inset: 0; 11 background: rgba(0, 0, 0, 0.5); 12 z-index: 999; 13} 14 15/* Disable interactions behind modal */ 16body:has(.modal.open) main { 17 pointer-events: none; 18 filter: blur(2px); 19}

Accordion/Disclosure#

1/* Accordion item with open details */ 2.accordion-item:has(details[open]) { 3 background-color: #f9fafb; 4 border-color: #3b82f6; 5} 6 7/* Rotate icon when open */ 8.accordion-item:has(details[open]) .icon { 9 transform: rotate(180deg); 10} 11 12/* Only one open at a time visual */ 13.accordion:has(details[open]) details:not([open]) { 14 opacity: 0.7; 15}

Empty States#

1/* Container with no visible children */ 2.container:has(> :not([hidden]):only-child) { 3 min-height: 200px; 4} 5 6/* List with no items */ 7ul:not(:has(li)) { 8 display: none; 9} 10 11/* Show empty state */ 12.list-container:not(:has(.list-item)) .empty-state { 13 display: flex; 14} 15 16.list-container:has(.list-item) .empty-state { 17 display: none; 18}

Filter UI#

1/* Show clear button when filters active */ 2.filters:has(input:checked) .clear-filters { 3 display: block; 4} 5 6.filters .clear-filters { 7 display: none; 8} 9 10/* Active filter count */ 11.filters:has(input:checked) .filter-count { 12 display: inline-flex; 13} 14 15/* Highlight active filter section */ 16.filter-group:has(input:checked) { 17 background-color: #eff6ff; 18 border-radius: 8px; 19 padding: 1rem; 20}

Responsive Behavior#

1/* Different layout based on content */ 2.hero:has(video) { 3 min-height: 100vh; 4} 5 6.hero:has(img):not(:has(video)) { 7 min-height: 60vh; 8} 9 10/* Sidebar behavior */ 11main:has(aside) { 12 display: grid; 13 grid-template-columns: 1fr 300px; 14} 15 16main:not(:has(aside)) { 17 max-width: 800px; 18 margin: 0 auto; 19}

Complex Combinations#

1/* Card with image AND video */ 2.card:has(img):has(video) { 3 display: grid; 4 grid-template-areas: 5 "media" 6 "content"; 7} 8 9/* NOT containing certain elements */ 10.section:not(:has(h2)):not(:has(h3)) { 11 padding: 1rem; 12} 13 14/* Deeply nested selection */ 15.page:has(form:invalid) .submit-section { 16 opacity: 0.5; 17 pointer-events: none; 18} 19 20/* Multiple conditions */ 21article:has(> header):has(> footer) { 22 border: 1px solid #e5e7eb; 23 border-radius: 12px; 24}

Performance Tips#

1/* More specific = better performance */ 2/* Good: specific selector */ 3.card:has(> img) { } 4 5/* Avoid: very broad selector */ 6:has(img) { } 7 8/* Combine with classes when possible */ 9.gallery:has(.selected) { } 10 11/* Use direct child when applicable */ 12ul:has(> li.active) { }

Best Practices#

Use Cases: ✓ Parent styling based on children ✓ Form validation states ✓ Dynamic layouts ✓ Sibling relationships ✓ State-based styling Performance: ✓ Be specific with selectors ✓ Use direct child (>) when possible ✓ Combine with class selectors ✓ Avoid overly broad :has() Patterns: ✓ Form group validation ✓ Card content detection ✓ Navigation active states ✓ Empty/loading states Browser Support: ✓ Check caniuse.com ✓ Provide fallbacks ✓ Use @supports for detection ✓ Progressive enhancement

Conclusion#

The :has() selector revolutionizes CSS by enabling parent selection based on descendants. Use it for form validation styling, dynamic layouts based on content, navigation states, and quantity queries. Combine with :not() for inverse selection and be mindful of performance by keeping selectors specific. This powerful selector eliminates many JavaScript-based styling solutions while keeping styles declarative and maintainable.

Share this article

Help spread the word about Bootspring