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.

The basic syntax
/* 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.

๐Ÿ’ก Pro Tip
Inside @supports selector(), you don't need to test your exact selector โ€” :has(*) is enough. The browser checks whether the syntax is recognized, not whether any elements actually match.

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.

Switch card layout based on whether an image exists
/* 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;
}
โ–ถ Live Demo โ€” Card layout adapts to content
Sample image

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.

Sample photo

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-level validation feedback
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;
}
โ–ถ Live Demo โ€” Form validation with :has()
Try entering invalid data โ€” the form border turns red and the button is disabled
๐Ÿ“Œ Key Point
You can pass any pseudo-class into :has() โ€” :checked, :focus-visible, :hover, :disabled, you name it. The ability to propagate a child's state up to the parent is what makes :has() transformative.

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.

Selecting the element *before* a target
/* 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.

โ–ถ Live Demo โ€” Hover ripple effect in both directions
Home
About
Works
Blog
Contact

Hover any item โ€” the effect ripples outward to neighboring items in both directions

The core CSS behind the ripple demo
/* 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.

Scroll lock with zero JavaScript
/* 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.

Dark mode with :has() and CSS custom properties
/* 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;
}
โ–ถ Live Demo โ€” JS-free dark mode toggle

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.

โš ๏ธ Watch Out
This JS-free dark mode is a fantastic demo, but a production implementation should also persist the user's preference in localStorage and respect the OS-level prefers-color-scheme setting. The best approach is a hybrid: let CSS handle the visual theme swap via :has(), and use a small JavaScript helper to manage persistence and initial state.

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.

Style a parent based on child count
/* 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);
}
โ–ถ Live Demo โ€” Grid layout changes with item count

2 items โ†’ 2 columns / 4+ items โ†’ 4 columns / 7+ items โ†’ 3 columns. Click the buttons to see the grid reconfigure.

1
2
3
4
5

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.

Dim all siblings except the hovered one
/* When any card is hovered, blur the rest */
.card-list:has(.card:hover) .card:not(:hover) {
  filter: blur(3px);
  opacity: 0.6;
}
โ–ถ Live Demo โ€” Focus spotlight on hover
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.

โŒ Nesting :has() โ€” this is invalid
/* You CANNOT nest :has() inside :has() */
.parent:has(.child:has(span)) {
  color: red;
  /* โŒ Invalid โ€” the browser ignores this entirely */
}
โœ… Chaining :has() โ€” this works
/* Chain multiple :has() conditions instead */
.parent:has(.child):has(span) {
  color: red;
  /* โœ… Valid โ€” .parent must contain BOTH .child AND span */
}
๐Ÿ’ก Pro Tip
The litmus test I use in every project: "Will the next developer (or my future self) understand this selector in five seconds?" If a :has() chain is getting long enough that you need to read it twice, it's probably time to solve that problem in JavaScript instead. Use :has() for clear, declarative relationships โ€” not as a replacement for application logic.

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.