The Family Selector’s Real-World Payoff

Front-end CSS often boils down to writing rules for elements based on where they sit in the document — or, more precisely, what comes after them. The :has() pseudo-class flips that around, letting you style an element based on what it contains or what follows it. And while a lot of the early discussion has rightly focused on flashy demos and clever selector tricks, the day-to-day value of :has() might actually be in the quieter, more mundane parts of a project: cleaning up JavaScript, simplifying templates, and keeping specificity sane.

For developers who have been waiting on browser support before putting :has() to work in production, the time is nearly here. Firefox is the final holdout, but its implementation is imminent. When that ship lands, here are a few practical patterns worth reaching for.

Decoupling Components From the DOM

Interactive components often need to have an effect on styles outside their own markup. A classic case: a site header has a mega menu inside a <nav>. When the menu opens, the header’s colors should shift to match the open state. In a React (or any component-based) architecture, that often means reaching outside the component with something like document.querySelector() to toggle a class on the <body> or <header> — or, in a Next.js-style setup, lifting a menuIsOpen state much higher up the tree than feels natural.

With :has(), that external DOM manipulation is unnecessary. The parent element can respond to the state of a child directly:

header:has(.megamenu--open) {
  /* style the header differently if it contains 
    an element with the class ".megamenu--open"
  */
}

This removes the need to coordinate style state between unrelated pieces of the page, keeping your JavaScript cleaner and less brittle.

More Thoughtful Table Striping

Alternating row colors in tables can genuinely improve readability by helping the eye track across a long set of rows. But the effect falls apart on short tables. If a <tbody> has just three rows and “even” rows are highlighted, you end up with one lonely stripe — a visual artifact that offers no navigational benefit and risks looking like an error.

Using the same :has() technique for counting children that has been popularized for other use cases, you can make table striping conditional on the table having enough rows to justify the pattern. You can extend that logic to columns as well, so the striped effect only kicks in when both the row and column counts cross a useful threshold:

table:has(:is(td, th):nth-child(3)) {
  /* only do stuff if there are three or more columns */
}

That kind of conditional styling was previously only possible with JavaScript or by adding utility classes in the markup. Now it’s a one-line concern in CSS.

Cutting Conditional Classes From Templates

Layouts often need to shift based on the presence of content. Consider a grid where the placement of the main content area depends on whether a sidebar exists on the page. In a CMS-driven site, that presence is frequently determined at the template level:

Layout with left sidebar above a layout with no sidebar.

Historically, that meant writing template logic to conditionally add BEM modifier classes to a layout wrapper — code that can get messy when multiple elements need different modifiers depending on context:

/* m = main content */
/* s = sidebar */
.standard-page--with-sidebar {
  grid-template-areas: 's s s m m m m m m m m m';
}
.standard-page--without-sidebar {
  grid-template-areas: '. m m m m m m m m m . .';
}

That approach works, but it pushes presentational decisions into the template layer and makes views harder to read. A :has() selector allows the layout to respond natively to its own content:

/* m = main content */
/* s = sidebar */
.standard-page:has(.sidebar) {
  grid-template-areas: 's s s m m m m m m m m m';
}
.standard-page:not(:has(.sidebar)) {
  grid-template-areas: '. m m m m m m m m m . .';
}

The CSS is not dramatically shorter, but the win is in the HTML. Toolbars, wrappers, and page sections no longer need to carry layout-state classes in the markup. Conditional presentation stays in the stylesheet, where it belongs.

Keeping Specificity In Check

One of the easiest traps with :has() is specificity inflation. The specificity of the pseudo-class is calculated using the most specific element in its argument list. If an ID ends up inside a :has(), the entire selector becomes effectively impossible to override without resorting to !important or even gorier hacks.

The remedy is the :where() pseudo-class, which always contributes zero to the specificity score. Wrapping a :has() argument in :where() neutralizes its weight while keeping its functional power:

/* specificity score: 0,1,0.
  Same as a .standard-page--with-sidebar 
  modifier class
*/
.standard-page:where(:has(.sidebar)) {
  /* etc */
}

Without something like this, a single :has() rule scattered through a large stylesheet can quietly make future overrides much harder to write. With it, the added capability comes at no specificity cost.

The real promise of :has() extends beyond the eye-catching demos. For everyday work, it offers a way to push layout concerns out of JavaScript and templates, to write styles that are conditional on content in a more natural way, and — with a little :where() discipline — to do all of that without breaking the cascade.