CSS :has() — The Parent Selector That Changes Everything
Finally select parents based on children. Use :has() for conditional styling, previous sibling selection, and patterns that previously required JavaScript.
For two decades, CSS couldn't select parents based on their children. JavaScript filled the gap.
Now :has() changes everything—style any element based on what it contains,
no JS required. It's not just a parent selector; it's a relational selector that unlocks
patterns previously impossible in pure CSS.
The Basics: Selecting Parents
/* Select any <a> that contains an <img> */
a:has(img) {
display: block;
padding: 0;
}
/* Select <label> when its input is focused */
label:has(input:focus) {
color: var(--accent);
}
/* Select <form> that has invalid fields */
form:has(:invalid) {
border-left: 3px solid red;
}
The pattern is simple: element:has(selector) matches element
if selector matches something inside it. But :has() does more
than parent selection.
Beyond Parents: Previous Sibling Selection
CSS has + and ~ for next siblings, but no way to select
previous siblings. :has() fixes that:
/* Select the label BEFORE a checked checkbox */
label:has(+ input:checked) {
font-weight: bold;
}
/* Select any li before a li with .active */
li:has(~ li.active) {
opacity: 0.5;
}
/* Select h2 if followed by a paragraph */
h2:has(+ p) {
margin-bottom: 0.5rem;
}
The selector inside :has() is relative to the element being tested,
so combinators like + and ~ work from that element outward.
Practical Patterns
1. Form Validation States
/* Fieldset with any invalid input */
fieldset:has(:invalid) {
border-color: var(--error);
}
/* Form row when its input is focused */
.form-row:has(:focus-within) {
background: var(--highlight);
}
/* Submit button state based on form validity */
form:has(:invalid) button[type="submit"] {
opacity: 0.5;
pointer-events: none;
}No JavaScript event listeners, no state management—the styles react automatically to the form's validity state.
2. Conditional Layouts
/* Card layout changes when it has an image */
.card:has(img) {
display: grid;
grid-template-columns: 200px 1fr;
}
.card:not(:has(img)) {
padding: 1.5rem;
}
/* Navigation with dropdown */
nav:has(.dropdown:hover) {
position: relative;
}
nav li:has(.dropdown):hover > .dropdown {
display: block;
}3. Empty State Handling
/* Show empty message when list has no visible items */
.list:not(:has(li:not([hidden]))) {
display: flex;
justify-content: center;
}
.list:not(:has(li:not([hidden])))::after {
content: "No items to display";
color: var(--muted);
}4. Quantity Queries
/* Different layouts based on number of items */
.grid:has(> :nth-child(4)) {
grid-template-columns: repeat(2, 1fr);
}
.grid:has(> :nth-child(7)) {
grid-template-columns: repeat(3, 1fr);
}
/* Single item gets full width */
.grid:not(:has(> :nth-child(2))) > * {
grid-column: 1 / -1;
}Chaining and Combining :has()
/* Multiple conditions (AND logic) */
.card:has(img):has(.featured-badge) {
border: 2px solid gold;
}
/* Argument list (OR logic) */
.card:has(img, video) {
aspect-ratio: 16 / 9;
}
/* Nested :has() */
article:has(header:has(h1)) {
/* Article with a header that contains an h1 */
}
/* Combined with :not() */
button:not(:has(svg)):not(:has(img)) {
/* Text-only buttons */
padding-inline: 1.5rem;
}Performance Considerations
:has() requires browsers to check descendants, which sounds expensive.
In practice, browser implementations are heavily optimized:
- Caching: WebKit caches
:has()results and invalidates intelligently - Bloom filters: Quick rejection of non-matching elements
- Limit scope: Use
:has(> child)instead of:has(descendant)when possible
/* ✅ Faster: direct child check */
.container:has(> .active) { }
/* ⚠️ Slower: descendant search */
.container:has(.active) { }
/* ⚠️ Avoid in hot paths */
*:has(.something) { } /* Checks every element */
Profile if you use :has() extensively, but for typical use cases,
the performance is excellent—WebKit's blog reports sub-millisecond matching
even on complex pages.
Edge Cases and Gotchas
:has() Cannot Match Itself
/* This doesn't select divs that ARE .active */
div:has(.active) { } /* Selects divs CONTAINING .active */
/* For self-matching, use :is() or regular selectors */
div.active { }
div:is(.active) { }Specificity
:has() contributes 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) { }Pseudo-elements Don't Work
/* ❌ Invalid - can't select based on pseudo-elements */
div:has(::before) { }
/* ❌ Invalid - pseudo-elements can't use :has() */
div::before:has(.foo) { }Real-World Example: Dynamic Navigation
<nav>
<ul>
<li><a href="/">Home</a></li>
<li>
<a href="/products">Products</a>
<ul class="dropdown">
<li><a href="/products/new">New</a></li>
<li><a href="/products/sale">Sale</a></li>
</ul>
</li>
</ul>
</nav>/* Add dropdown indicator only when dropdown exists */
nav li:has(.dropdown) > a::after {
content: " ▾";
}
/* Show dropdown on parent hover */
nav li:has(.dropdown):hover .dropdown {
display: block;
}
/* Style parent differently when dropdown is open */
nav li:has(.dropdown:hover) > a {
background: var(--nav-active);
}
/* Highlight entire nav when any dropdown is open */
nav:has(.dropdown:hover) {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}Browser Support
:has() is supported in all evergreen browsers since late 2023 (Chrome 105+,
Safari 15.4+, Firefox 121+). For legacy support, use feature detection:
@supports selector(:has(*)) {
/* :has() styles */
}
@supports not selector(:has(*)) {
/* Fallback styles or JS enhancement */
}Key Takeaways
:has()selects elements based on their descendants—true parent selection- With combinators, it enables previous sibling selection (
:has(+ sibling)) - Combine with
:not(),:is(), and other selectors for complex logic - Performance is excellent in modern browsers—use direct child (
>) when possible - Eliminates JavaScript for many conditional styling patterns
Advertisement
Explore these curated resources to deepen your understanding
Official Documentation
Tools & Utilities
Related Insights
Explore related edge cases and patterns
Advertisement