EdgeCases Logo
Feb 2026
CSS
Surface
6 min read

CSS :has() — The Parent Selector That Changes Everything

Finally select parents based on children. Use :has() for conditional styling, previous sibling selection, and patterns that previously required JavaScript.

css
has-selector
parent-selector
selectors
forms
navigation
no-javascript

For two decades, CSS couldn't select parents based on their children. JavaScript filled the gap. Now :has() changes everything—style any element based on what it contains, no JS required. It's not just a parent selector; it's a relational selector that unlocks patterns previously impossible in pure CSS.

The Basics: Selecting Parents

/* Select any <a> that contains an <img> */
a:has(img) {
  display: block;
  padding: 0;
}

/* Select <label> when its input is focused */
label:has(input:focus) {
  color: var(--accent);
}

/* Select <form> that has invalid fields */
form:has(:invalid) {
  border-left: 3px solid red;
}

The pattern is simple: element:has(selector) matches element if selector matches something inside it. But :has() does more than parent selection.

Beyond Parents: Previous Sibling Selection

CSS has + and ~ for next siblings, but no way to select previous siblings. :has() fixes that:

/* Select the label BEFORE a checked checkbox */
label:has(+ input:checked) {
  font-weight: bold;
}

/* Select any li before a li with .active */
li:has(~ li.active) {
  opacity: 0.5;
}

/* Select h2 if followed by a paragraph */
h2:has(+ p) {
  margin-bottom: 0.5rem;
}

The selector inside :has() is relative to the element being tested, so combinators like + and ~ work from that element outward.

Practical Patterns

1. Form Validation States

/* Fieldset with any invalid input */
fieldset:has(:invalid) {
  border-color: var(--error);
}

/* Form row when its input is focused */
.form-row:has(:focus-within) {
  background: var(--highlight);
}

/* Submit button state based on form validity */
form:has(:invalid) button[type="submit"] {
  opacity: 0.5;
  pointer-events: none;
}

No JavaScript event listeners, no state management—the styles react automatically to the form's validity state.

2. Conditional Layouts

/* Card layout changes when it has an image */
.card:has(img) {
  display: grid;
  grid-template-columns: 200px 1fr;
}

.card:not(:has(img)) {
  padding: 1.5rem;
}

/* Navigation with dropdown */
nav:has(.dropdown:hover) {
  position: relative;
}

nav li:has(.dropdown):hover > .dropdown {
  display: block;
}

3. Empty State Handling

/* Show empty message when list has no visible items */
.list:not(:has(li:not([hidden]))) {
  display: flex;
  justify-content: center;
}

.list:not(:has(li:not([hidden])))::after {
  content: "No items to display";
  color: var(--muted);
}

4. Quantity Queries

/* Different layouts based on number of items */
.grid:has(> :nth-child(4)) {
  grid-template-columns: repeat(2, 1fr);
}

.grid:has(> :nth-child(7)) {
  grid-template-columns: repeat(3, 1fr);
}

/* Single item gets full width */
.grid:not(:has(> :nth-child(2))) > * {
  grid-column: 1 / -1;
}

Chaining and Combining :has()

/* Multiple conditions (AND logic) */
.card:has(img):has(.featured-badge) {
  border: 2px solid gold;
}

/* Argument list (OR logic) */
.card:has(img, video) {
  aspect-ratio: 16 / 9;
}

/* Nested :has() */
article:has(header:has(h1)) {
  /* Article with a header that contains an h1 */
}

/* Combined with :not() */
button:not(:has(svg)):not(:has(img)) {
  /* Text-only buttons */
  padding-inline: 1.5rem;
}

Performance Considerations

:has() requires browsers to check descendants, which sounds expensive. In practice, browser implementations are heavily optimized:

  • Caching: WebKit caches :has() results and invalidates intelligently
  • Bloom filters: Quick rejection of non-matching elements
  • Limit scope: Use :has(> child) instead of :has(descendant) when possible
/* ✅ Faster: direct child check */
.container:has(> .active) { }

/* ⚠️ Slower: descendant search */
.container:has(.active) { }

/* ⚠️ Avoid in hot paths */
*:has(.something) { } /* Checks every element */

Profile if you use :has() extensively, but for typical use cases, the performance is excellent—WebKit's blog reports sub-millisecond matching even on complex pages.

Edge Cases and Gotchas

:has() Cannot Match Itself

/* This doesn't select divs that ARE .active */
div:has(.active) { } /* Selects divs CONTAINING .active */

/* For self-matching, use :is() or regular selectors */
div.active { }
div:is(.active) { }

Specificity

:has() contributes specificity based on its most specific argument:

/* Specificity: (0, 1, 1) - element + class */
div:has(.foo) { }

/* Specificity: (1, 0, 1) - element + ID */
div:has(#bar) { }

/* Specificity: (0, 2, 0) - two classes */
:has(.foo.bar) { }

Pseudo-elements Don't Work

/* ❌ Invalid - can't select based on pseudo-elements */
div:has(::before) { }

/* ❌ Invalid - pseudo-elements can't use :has() */
div::before:has(.foo) { }

Real-World Example: Dynamic Navigation

<nav>
  <ul>
    <li><a href="/">Home</a></li>
    <li>
      <a href="/products">Products</a>
      <ul class="dropdown">
        <li><a href="/products/new">New</a></li>
        <li><a href="/products/sale">Sale</a></li>
      </ul>
    </li>
  </ul>
</nav>
/* Add dropdown indicator only when dropdown exists */
nav li:has(.dropdown) > a::after {
  content: " ▾";
}

/* Show dropdown on parent hover */
nav li:has(.dropdown):hover .dropdown {
  display: block;
}

/* Style parent differently when dropdown is open */
nav li:has(.dropdown:hover) > a {
  background: var(--nav-active);
}

/* Highlight entire nav when any dropdown is open */
nav:has(.dropdown:hover) {
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}

Browser Support

:has() is supported in all evergreen browsers since late 2023 (Chrome 105+, Safari 15.4+, Firefox 121+). For legacy support, use feature detection:

@supports selector(:has(*)) {
  /* :has() styles */
}

@supports not selector(:has(*)) {
  /* Fallback styles or JS enhancement */
}

Key Takeaways

  • :has() selects elements based on their descendants—true parent selection
  • With combinators, it enables previous sibling selection (:has(+ sibling))
  • Combine with :not(), :is(), and other selectors for complex logic
  • Performance is excellent in modern browsers—use direct child (>) when possible
  • Eliminates JavaScript for many conditional styling patterns

Advertisement

Related Insights

Explore related edge cases and patterns

CSS
Expert
The 16 Ways CSS Creates Stacking Contexts
8 min
CSS
Deep
Respecting User Motion Preferences with prefers-reduced-motion
7 min

Advertisement