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}Navigation States#
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}Modal and Overlay#
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.