CSS :has() Selector โ A Practical Guide to Styling Parents, Siblings, and Beyond
Style parent elements based on their children, select previous siblings, detect global state changes, and build quantity-aware layouts โ all in pure CSS, no JavaScript required. Here's everything you need to start using :has() in production today.

What :has() Actually Is โ and Why It Took So Long
For over a decade, "parent selector" sat at the very top of every CSS wishlist. The need was everywhere: style a card differently when it contains an image, highlight a form when any input is invalid, change a nav item when its dropdown is open. The answer was always the same โ reach for JavaScript. Until now.
The :has() pseudo-class is officially known as a "relational pseudo-class." It selects an element based on what it contains. In plain English: "pick the parent that has a matching child." That single idea unlocks a staggering number of patterns that simply weren't possible in CSS before.
/* Select any .card that contains an img */
.card:has(img) {
display: flex;
flex-direction: row;
}
/* Select a form that contains an invalid input */
form:has(input:invalid) {
border: 2px solid #ef4444;
}
Traditional CSS selectors flow in one direction โ top to bottom, parent to child. The + and ~ combinators can reach sideways, but only forward in the DOM. :has() shatters these constraints. It lets you style an element based on any descendant's existence, state, or position. It's the selector CSS was always missing.
Browser support in 2026
As of March 2026, :has() is fully supported in every major browser: Chrome 105+, Edge 105+, Safari 15.4+, and Firefox 121+. Global support on Can I Use sits at roughly 96%. Unless you're targeting truly legacy environments, you can ship :has() in production with confidence.
If you do need a fallback strategy, the @supports selector(:has(*)) at-rule lets you provide baseline styles for older browsers while progressively enhancing for modern ones. In my experience, most :has() use cases are enhancements rather than mission-critical layout shifts, so graceful degradation is straightforward.
Pattern 1: The Parent Selector
The most intuitive use of :has() is selecting a parent based on what's inside it. This is the "parent selector" the community has been requesting since CSS2, and it's every bit as useful as we imagined.
Adaptive card layouts based on content
Picture a blog listing where some cards have featured images and others don't. In the past, you'd either add a modifier class on the server or toggle one with JavaScript. With :has(), the HTML stays identical โ CSS handles the branching.
/* Card with an image โ horizontal layout */
.card:has(img) {
display: flex;
flex-direction: row;
gap: 20px;
}
/* Card without an image โ vertical with accent border */
.card:not(:has(img)) {
padding: 24px;
border-left: 4px solid #059669;
}
Card with image
Matches :has(img) โ flexbox kicks in and the card becomes a horizontal layout with image on the left.
Card without image
Matches :not(:has(img)) โ gets a left border accent and standard vertical padding. Same HTML structure, different visual treatment.
Another image card
Whether a CMS entry has a thumbnail or not, CSS automatically picks the right layout. No server-side conditionals needed.
Reactive form validation
Another pattern I reach for constantly in real projects: changing the entire form's appearance when validation fails. One selector, zero JavaScript event listeners.
form:has(input:invalid) {
border: 2px solid #ef4444;
background: #fef2f2;
}
/* Disable the submit button while any input is invalid */
form:has(input:invalid) .submit-btn {
opacity: 0.5;
pointer-events: none;
}
Pattern 2: The Previous Sibling Selector
Here's where things get really interesting. :has() isn't just a parent selector โ it can also target previous siblings, something that was flat-out impossible in CSS before.
The adjacent sibling combinator + and general sibling combinator ~ only work in one direction: forward in the DOM. But combine :has() with +, and suddenly you can select backward.
/* Select the label right before a checked input */
label:has(+ input:checked) {
color: #059669;
font-weight: 700;
}
/* Select a paragraph that comes right before a figure */
p:has(+ figure) {
margin-bottom: 8px;
}
The logic reads naturally once you see it: .item:has(+ .item:hover) means "an item whose next sibling is being hovered." In other words, the item before the hovered one. By combining this with the traditional + combinator for forward selection, you can create effects that radiate outward in both directions from a target element.
Hover any item โ the effect ripples outward to neighboring items in both directions
/* The hovered item itself */
.item:hover {
transform: scale(1.12);
}
/* One step back (previous sibling via :has) */
.item:has(+ .item:hover) {
transform: scale(1.05);
}
/* One step forward (traditional adjacent combinator) */
.item:hover + .item {
transform: scale(1.05);
}
/* Two steps back */
.item:has(+ .item + .item:hover) {
transform: scale(1.02);
}
This technique is particularly compelling for navigation menus, tab bars, star ratings, and timeline components โ anywhere you want hover or focus feedback to feel organic rather than isolated to a single element.
Pattern 3: Global State Detection
This might be the most mind-bending use case for :has(). By applying it to html or body, you essentially get a CSS-only global event listener. If any element anywhere in the DOM matches a condition, you can change styles on a completely unrelated element.
Scroll lock when a modal is open
In my React projects, I used to manage this with a useEffect hook that mutated document.documentElement.style.overflow. It worked, but it was clunky โ especially when multiple components needed to request a scroll lock simultaneously. With :has(), it's one line of CSS.
/* Lock scrolling whenever any modal is open */
html:has(.modal.is-open) {
overflow: hidden;
}
/* Or use a data attribute for explicit control */
html:has([data-scroll-lock="true"]) {
overflow: hidden;
}
JS-free dark mode toggle
The global detection pattern truly shines when combined with form controls. A checkbox anywhere in the DOM can flip an entire page's color scheme โ no JavaScript whatsoever.
/* Light mode (default) */
body {
--bg: #ffffff;
--text: #1a1a1a;
--surface: #f4f4f5;
}
/* Dark mode โ triggered by a checkbox */
body:has(#dark-toggle:checked) {
--bg: #0e0e0e;
--text: #e2e8f0;
--surface: #1e293b;
}
Sample Page
Article Title A
CSS custom properties combined with :has() enable a full theme toggle without a single line of JavaScript.
Article Title B
The :checked state is detected globally, so every element on the page reacts to the toggle automatically.
Pattern 4: Quantity Queries โ Responsive to Content Count
Combine :has() with :nth-child(), and you unlock something remarkably useful: styling a parent based on how many children it contains. The CSS community calls these "quantity queries," and they're a game-changer for content-driven layouts.
Think of a tag cloud that should stay inline when there are just a few items but wrap when there are many. Or a photo grid that switches from 2 columns to 3 columns once there are enough images to justify it. With :has(), the layout adapts to the content automatically โ no JavaScript counter, no CMS template logic.
/* 4 or more children */
.tag-list:has(> :nth-child(4)) {
flex-wrap: wrap;
}
/* Exactly 3 children */
.grid:has(> :nth-child(3):last-child) {
grid-template-columns: repeat(3, 1fr);
}
/* 6 or more children */
.grid:has(> :nth-child(6)) {
grid-template-columns: repeat(3, 1fr);
}
2 items โ 2 columns / 4+ items โ 4 columns / 7+ items โ 3 columns. Click the buttons to see the grid reconfigure.
What makes this pattern powerful in the real world is that it decouples layout decisions from content generation. Your CMS, API, or component can output any number of items โ and CSS figures out the best arrangement on its own.
Pattern 5: The "All But Me" Selector and Practical Boundaries
One more pattern that comes up constantly in production work: highlighting the element you're interacting with while dimming everything else. :has() combined with :not() makes this trivially easy.
/* When any card is hovered, blur the rest */
.card-list:has(.card:hover) .card:not(:hover) {
filter: blur(3px);
opacity: 0.6;
}
Project Alpha
Corporate site rebuild using modern CSS. Grid + :has() for adaptive card layouts.
Project Beta
E-commerce product listing. Automatic layout switching based on image availability.
Project Gamma
SaaS dashboard with real-time form validation powered by :has().
Hover a card โ everything else fades out to create a spotlight effect
Where to draw the line
:has() is incredibly powerful, but power demands discipline. Here are a few practical boundaries I've established after shipping it in several projects.
/* You CANNOT nest :has() inside :has() */
.parent:has(.child:has(span)) {
color: red;
/* โ Invalid โ the browser ignores this entirely */
}
/* Chain multiple :has() conditions instead */
.parent:has(.child):has(span) {
color: red;
/* โ
Valid โ .parent must contain BOTH .child AND span */
}
Takeaways
- :has() is a relational pseudo-class that selects elements based on their descendants. It's the "parent selector" CSS has needed for over a decade.
- As of 2026, it's supported in all major browsers (Chrome 105+, Edge 105+, Safari 15.4+, Firefox 121+) with ~96% global coverage. Production-ready.
- Use it as a parent selector to create content-adaptive layouts โ cards, forms, nav items โ without JavaScript class toggling.
- Combine with + to select previous siblings, unlocking bidirectional hover effects and ripple-style interactions.
- Apply to html or body for global state detection: scroll locks, theme toggles, and cross-component styling with zero JS.
- Pair with :nth-child() for quantity queries โ automatically adjust grid columns, spacing, and layout based on how many children exist.
- Remember: :has() can't be nested (but it can be chained). Prioritize readability and maintainability, and reach for JavaScript when the selector grows too complex.