CSS :has() - The Parent Selector We Never Had
Style parent elements based on their children. No JavaScript. No class name orchestration. No "just add a data attribute".
For years, CSS only flowed one way: down. If you wanted to style a parent based on its children, you wrote JavaScript or reshaped your HTML. Not anymore.
Demo 1: Card with optional image
Product Card
The card layout automatically adapts based on whether an image is present. No extra classes needed.
Demo 2: Form label reacts to input error
Demo 3: Nav highlights when it contains active link
đź’ˇ Browser support: Chrome 105+, Firefox 121+, Safari 15.4+. All modern browsers since 2023.
What is :has()?
The :has() pseudo-class lets you select an element if it contains a specific child or descendant.
Think of it as: "if this element has X inside, apply these styles".
/* Style a card differently if it contains an image */ .card:has(img) { display: grid; grid-template-columns: 120px 1fr; } /* Highlight a form group if the input has an error */ .form-group:has(input[aria-invalid="true"]) label { color: red; } /* Style nav differently when it contains an active link */ nav:has(.active) { border-color: var(--accent); background: var(--accent-subtle); }
It is the parent selector people have been asking for since about 2010. And it has been in modern browsers for a while now.
đź’¬ Council advice: How to explain :has() to your designer in one sentence
"It lets the card style itself differently when it has an image, so we do not have to add extra classes or wrapper divs." If they ask for details, show them the before/after code example. If they start asking about browser support or performance, tell them it works everywhere modern and is faster than the JavaScript we would have written instead. Do not let this turn into a 30-minute architectural discussion. It is a selector. It works. Ship it.
Why this matters
Before :has(), styling a parent based on its children meant one of these options:
- JavaScript - add/remove classes on the parent when children change
- Extra markup - wrap things in extra divs with special classes
- Give up - tell the designer it is not possible
- Preprocessor "solutions" - which do nothing at runtime
All of these are more complex and more fragile than writing the relationship directly in CSS.
Real-world examples
Card layouts that adapt
You have a card component. Sometimes it has an image, sometimes it does not.
Before :has():
<div className={image ? "card card--with-image" : "card"}> {image && <img src={image} alt="" />} <div className="card__content">...</div> </div>
You need conditional class names. Your CSS has two variations. Your component logic grows new branches.
With :has():
.card:has(img) { display: grid; grid-template-columns: 120px 1fr; gap: 1rem; }
The card automatically adjusts its layout when an image is present. No class juggling.
Form validation styling
You want the label to turn red when the input has a validation error.
Before :has():
<div className={error ? "form-group form-group--error" : "form-group"}> <label>Email</label> <input aria-invalid={!!error} /> </div>
You add an error class to the parent and keep it in sync.
With :has():
.form-group:has(input[aria-invalid="true"]) label { color: var(--error); } .form-group:has(input[aria-invalid="true"]) label::before { content: "âš "; }
The label reacts automatically to the input's aria-invalid state. Fewer moving parts.
Navigation with active state
You want the nav background to change when it contains an active link.
Before :has():
<nav className={hasActiveChild ? "nav nav--active" : "nav"}> <a className={isActive ? "active" : ""}>Blog</a> </nav>
You track whether any child is active and pass that back up.
With :has():
nav:has(.active) { background: var(--nav-active-bg); border-color: var(--accent); }
The nav styles itself based on its contents. Your components stay simpler.
When NOT to use :has()
:has() is powerful, which means it is also easy to overuse.
Skip it for:
- Simple child styling - if you only need to style the child, just select the child
- Deep nesting -
.parent:has(.child:has(.grandchild:has(...)))is hard to read and harder to run - Pure layout problems - use Grid/Flexbox for actual layout decisions
- Legacy-heavy projects - if you must support older Safari/Android browsers, keep a fallback
Use it for:
- Parent–child styling relationships
- Conditional layouts based on content presence
- Form state styling that follows HTML semantics
- Reducing JavaScript-driven class management
If you hear yourself saying "I just need the parent to know about this child", :has() is probably the tool.
Browser support
:has() is supported in:
- Chrome 105+
- Safari 15.4+
- Firefox 121+
- Edge 105+
In practice, that is nearly all current users.
Fallback strategy
If you need to support older browsers, use @supports:
/* Fallback layout */ .card { display: block; } /* Enhanced layout for browsers with :has() */ @supports selector(:has(*)) { .card:has(img) { display: grid; grid-template-columns: 120px 1fr; } }
Older browsers get the simple version. Newer ones get the nicer layout. Everybody gets something that works.
Performance
:has() is efficient when used with a bit of discipline.
The browser evaluates :has() during style calculation. Overly broad selectors mean more work:
- Avoid very generic patterns like
div:has(span) - Prefer direct children when possible:
.card:has(> img) - Keep chains shallow: you want relationships, not a regex engine
The patterns in this article are all scoped and cheap. If your selector reads like a legal clause, it probably needs to be simplified.
Common patterns
Optional elements
Style a container based on whether optional content is present:
/* Article with/without a hero image */ article:has(.hero) { padding-top: 0; } /* Card with/without a footer */ .card:has(.card-footer) { padding-bottom: 0; }
State-based styling
React to semantic HTML state without extra classes:
/* Form field with error */ .field:has(input:invalid) { border-color: red; } /* Checkbox group with at least one checked */ .checkbox-group:has(input:checked) { background: var(--selected-bg); }
Combining selectors
Use :has() with other pseudo-classes for precise targeting:
/* Nav with active link */ nav:has(.active) { background: var(--nav-active-bg); } /* Table row that contains a selected checkbox */ tr:has(input[type="checkbox"]:checked) { background: var(--row-selected); }
These are the workhorse cases you will use all the time.
Try it yourself
The demo above uses these exact techniques. Toggle the checkboxes, watch the parent styles react, then copy whichever selector matches your use case.
/* Card with image */ .card:has(img) { display: grid; grid-template-columns: 120px 1fr; } /* Form with error */ .form-group:has(input[aria-invalid="true"]) label { color: red; } /* Nav with active link */ nav:has(.active) { border-color: var(--accent); }
Three patterns. Zero JavaScript. Fully semantic. Boring reliability.
Resources
- MDN: :has() - official documentation
- Can I Use: :has() - browser support table
- WebKit Blog: :has() pseudo-class - deep dive from the Safari team
Copy. Paste. Ship. The parent selector finally made it into CSS.