CSS :has() Selector: The Parent Selector You've Been Waiting For
For twenty years, CSS had a rule you just accepted: you could select children based on parents, but never parents based on children. You wanted to style a card differently when it contained an image? JavaScript. A form field that had a validation error? JavaScript. A nav item with a submenu? JavaScript, again.
Then :has() shipped, and the answer to all three is now one CSS selector. It's the closest thing CSS has to a "parent selector," it's supported in every major browser since late 2023, and it quietly removes a whole category of JavaScript from your codebase.
What :has() actually is
:has() is a relational selector: it matches an element if it has a certain descendant, sibling, or relative. The most famous use is selecting a parent based on its children:
/* A card that contains an image gets a different layout */
.card:has(> img) {
grid-template-columns: 120px 1fr;
}
/* A form field that currently has an error */
.field:has(.error-message) {
border-color: #dc2626;
}
Read it out loud: "any .card that has an image child gets this style." No class toggling from JavaScript. No watching for validation events. The CSS reflects the DOM state as it actually is.
The patterns that kill real JavaScript
These are the cases where :has() replaced actual JS in my projects:
1. Validation states without a state manager
.input-group:has(input:invalid) {
border-color: #dc2626;
}
.input-group:has(input:valid) {
border-color: #16a34a;
}
The moment the browser applies native validation states, the container restyles itself. Zero JavaScript, zero observers.
2. Cards that react to their content
.media-card:has(img) { display: grid; grid-template-columns: 120px 1fr; }
.media-card:not(:has(img)) { display: block; padding: 1.5rem; }
The same card component adapts whether or not it contains an image — which is exactly the kind of component-level behavior container queries and modern CSS are moving toward.
3. Nav items with submenus
.nav-item:has(ul)::after {
content: '▾';
}
The arrow appears only on items that actually have a submenu. Previously this was a class added by the menu script, or an arrow on every item.
4. Layout that depends on content presence
.page:has(.banner) {
padding-top: 0; /* banner already provides spacing */
}
The gotchas nobody warns you about
1. Pseudo-elements don't work inside :has()
You can't do :has(::before) or :has(::after). The relative selector doesn't accept pseudo-elements as its target. It's a hard browser limitation, not a workaround situation.
2. There's a real performance cost
:has() isn't free. The browser has to scan descendants to evaluate the match, and while browsers optimize aggressively, the cost scales with how many elements match your subject. The practical rules:
- Keep the selector inside the parentheses specific.
.card:has(> img)beats.card:has(img)on a big page because the child combinator limits the scan. - Don't wrap huge sections in
:has()checks that fire on every layout. - It's fine on forms, cards, and menus. It's not the tool for "does the whole page contain X anywhere."
3. Specificity got higher
:has() adds to the specificity of the selector it's attached to, and it can push you into specificity territory that's hard to override later. If you find yourself fighting your own :has() rules, a class is often the cleaner answer.
Browser support: the good news
:has() shipped in Chrome and Edge 105, Safari 15.4, and Firefox 121 — that last one landed in December 2023, which is when it became safe to use everywhere. As of 2026 it's a baseline feature, not a progressive enhancement. If you're not supporting genuinely ancient browsers, you can just use it.
Where it pairs with other modern CSS
The fun part is combining :has() with the rest of the modern toolkit. A card that uses :has() to detect its content, container queries to size itself, and @layer to keep specificity sane — that's a component that needs no JavaScript and no override classes. If you're already moving toward container queries for components, :has() is the natural companion for content-aware behavior.
Once you start collecting these modern CSS patterns — the :has() tricks, the container query cards, the layout skeletons — it's worth keeping them somewhere you can find them again. I save every pattern I end up reusing in Snippet Ark so the next project starts from the working version instead of a blank file.
The bottom line
:has() is the parent selector CSS never had, and it deletes a surprising amount of JavaScript from everyday UI: validation styling, content-aware components, menu affordances. Learn it once, keep the selectors specific, and it'll be one of those features you wonder how you lived without — right up there with container queries and logical properties.