Use :has() to style parent elements based on their descendants
rule · has-selector
The :has() pseudo-class (opens in a new tab) selects an element if any selector passed as an argument matches a descendant or related element. The web.dev guide (opens in a new tab) is why people call it the long-awaited parent selector: many JavaScript class-toggling patterns become pure CSS.
Code Example
/* Select .card if it contains an img element */
.card:has(img) {
display: grid;
grid-template-columns: 200px 1fr;
}
/* Select a <p> that is immediately followed by an <ul> */
p:has(+ ul) {
margin-bottom: 0.25rem;
}
/* Select a <section> that contains a focused element */
section:has(:focus-visible) {
outline: 2px solid var(--color-focus);
}Why It Matters
Before :has(), styling a parent based on its children required JavaScript: detecting state, toggling classes, and keeping DOM and styles in sync. This created coupling between styling logic and application logic. :has() moves that relationship back into CSS where it belongs, reducing JavaScript complexity, eliminating class-toggling boilerplate, and making styling intent readable in the stylesheet itself.
Form Styling Based on Input State
Replace JavaScript class toggling with CSS state:
/* Style the form group when its input is invalid and not empty */
.form-group:has(input:invalid:not(:placeholder-shown)) {
--border-color: var(--color-error);
}
.form-group:has(input:invalid:not(:placeholder-shown)) .form-label {
color: var(--color-error);
}
.form-group:has(input:invalid:not(:placeholder-shown)) .form-error {
display: block; /* show the error message */
}
/* Style a fieldset when all its required inputs are valid */
fieldset:has(input[required]:valid):not(:has(input[required]:invalid)) {
border-color: var(--color-success);
}
/* Checkbox-driven section toggle — no JavaScript */
.settings-panel:has(input[type="checkbox"]:checked) .settings-detail {
display: block;
}Card Layout Based on Content Presence
/* Cards without images stack their content differently */
.card:has(img) {
grid-template-areas:
'image'
'content'
'footer';
}
.card:not(:has(img)) {
grid-template-areas:
'content'
'footer';
}
/* Card with a badge gets extra top padding */
.card:has(.badge) {
padding-top: 2.5rem;
}
/* Article with a figure gets a 2-column layout */
.article:has(figure) {
display: grid;
grid-template-columns: 1fr 280px;
gap: 2rem;
}Navigation Active State
/* Highlight the nav item whose link is the current page */
.nav-item:has(a[aria-current="page"]) {
background: var(--color-nav-active-bg);
border-radius: 0.375rem;
}
.nav-item:has(a[aria-current="page"]) a {
color: var(--color-nav-active-text);
font-weight: 600;
}
/* Expand a nav section if it contains the active link */
.nav-section:has(a[aria-current="page"]) .nav-section-list {
display: block;
}Theme Switching Without JavaScript
A classic use case: a <select> or <input> that controls a whole-page theme:
/* Page theme driven by a radio button — no JS required */
html:has(#theme-dark:checked) {
color-scheme: dark;
--bg: #0f172a;
--fg: #f1f5f9;
}
html:has(#theme-light:checked) {
color-scheme: light;
--bg: #ffffff;
--fg: #0f172a;
}:has() with Relational Combinators
:has() accepts the full selector grammar inside the parentheses, including combinators:
/* Direct child */
.parent:has(> .direct-child) { … }
/* Adjacent sibling (the element has a sibling after it) */
h2:has(+ p) { margin-bottom: 0.5rem; }
/* Subsequent sibling */
.alert:has(~ .alert) { /* there is another alert after this one */ }
/* Attribute selector */
figure:has(figcaption[hidden]) { /* caption is hidden */ }Browser Support
:has() reached baseline support in late 2023. All major browsers support it:
- Chrome/Edge 105+
- Firefox 121+
- Safari 15.4+
Use Can I Use (opens in a new tab) and @supports selector(:has(*)) to provide a fallback for older environments:
/* Base styles work for everyone */
.card {
display: block;
}
/* Enhanced layout only when :has() is available */
@supports selector(:has(*)) {
.card:has(img) {
display: grid;
grid-template-columns: 200px 1fr;
}
}Verification
Check the result against Can I Use (opens in a new tab) and the MDN :has() reference (opens in a new tab) so you confirm both dynamic behavior and fallback coverage.
- Open browser DevTools and inspect an element targeted by a
:has()rule. Confirm the rule appears in the Styles panel and is applied when the matching descendant is present. - Test with the matching descendant removed to confirm the style is not applied — verifying the selector responds dynamically to DOM changes.
- Check the browser's compatibility table or
@supports selector(:has(*))to confirm the feature is available for your target audience, or that a fallback is in place. - Profile the page with DevTools Performance panel if
:has()is used on elements that update frequently — confirm no excessive style recalculation time.