Use :has() to style parent elements based on their descendants
Use the CSS :has() relational pseudo-class to select and style an element based on what it contains, replacing JavaScript DOM manipulation for many common styling scenarios.
- :has() selects an element when a matching descendant exists inside it
- It is the parent selector CSS has needed for decades
- Baseline support since late 2023 — all major browsers include it
- Avoid complex :has() selectors in hot reflow paths — they can affect layout performance
Rule Details
The :has() pseudo-class (opens in new tab) selects an element if any selector passed as an argument matches a descendant or related element. The web.dev guide (opens in 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 */ }:has() with complex descendant selectors can be expensive when the browser needs to recalculate styles during frequent reflows (scroll, resize, animation). Avoid deeply nested or very broad :has() selectors on elements that repaint frequently. Prefer scoping :has() to a specific subtree (e.g., .card:has(img)) rather than a universal or tag-only selector at the root.
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 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 new tab) and the MDN :has() reference (opens in 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.
Use with AI
Copy these prompts to use with your AI assistant, or install the MCP server to use directly from Claude, Cursor, or Windsurf.
Check
Verify implementation
Look for patterns in this code where JavaScript toggles a class on a parent element based on the state or content of a child element. These are candidates for replacement with :has().
Fix
Auto-fix issues
Replace the JavaScript class-toggling logic with a CSS :has() selector. Show the selector that targets the parent element when the relevant child condition is met.
Explain
Learn more
Explain how the :has() pseudo-class works, how it differs from :is() and :not(), what descendant vs child combinators mean inside :has(), and how browser support stands today.
Review
Code review
Review the stylesheets and component JavaScript in this file. Flag any locations where a parent element's style is controlled by toggling a class from JavaScript based on child state — and show the equivalent :has() rule.