CSS :has() Selector: Complete Guide with Real Examples

:has() is the CSS parent selector developers waited 20 years for. Complete guide with 10 real-world examples, current browser support, performance notes, and the Tailwind has-[] variant.

Published: 2026-05-10

For 20 years, CSS could style children based on their parents but never the other way around. You could style a button inside a form, but you could not style the form because it had a button inside it. The :has() pseudo-class fixed that. Browsers shipped it through 2022 and 2023, and by April 2026 it works in roughly 91% of global browsers.

This guide covers what :has() actually does, the syntax with examples, ten real-world patterns it unlocks, the current browser support picture, performance tradeoffs, and the Tailwind v3.4+ has-[] variant.

What :has() does

:has() lets you style a parent based on its descendants. The selector takes another selector as an argument and matches any element that contains at least one descendant matching that argument.

/* Style a card when it contains an image */
.card:has(img) {
  padding: 0;
}

The .card selector matches as before; the :has(img) tail says "only when it contains an img descendant." Without :has(), you would solve this by adding a class on every card that has an image, either in JavaScript or in your template logic. With :has(), CSS does it for you.

It is also called the parent selector informally, but that undersells it. :has() works with any descendant relationship, and combined with sibling combinators (+, ~) you get effects that previously required JavaScript.

Syntax in six forms

/* 1. Has a descendant */
.parent:has(.child) { ... }

/* 2. Has a direct child only */
.parent:has(> .child) { ... }

/* 3. Has a next sibling */
.element:has(+ .next-sibling) { ... }

/* 4. Has any sibling later in the DOM */
.element:has(~ .later-sibling) { ... }

/* 5. Has multiple things (logical AND) */
form:has(input:invalid):has(button[type="submit"]) { ... }

/* 6. Has any of several things (selector list) */
.card:has(img, video, iframe) { ... }

A few important rules:

Ten real-world examples

1. Cards with images get different padding.

.card { padding: 1rem; }
.card:has(img) { padding: 0; }
.card:has(img) .content { padding: 1rem; }

2. Form validity styling without JavaScript.

form:has(input:invalid) button[type="submit"] {
  opacity: 0.5;
  pointer-events: none;
}

form:has(input:user-invalid) {
  border-color: red;
}

Prefer :user-invalid over :invalid so you avoid styling fields the user has not interacted with yet.

3. Empty-state styling.

.list:not(:has(.list-item)) {
  display: grid;
  place-items: center;
  min-height: 200px;
}

.list:not(:has(.list-item))::before {
  content: "Nothing here yet.";
  color: var(--muted);
}

The :not(:has(...)) idiom replaces the JS that detects empty containers.

4. Mark the label of a required input.

label:has(input:required)::after {
  content: " *";
  color: red;
}

5. Hover on a child applies to the parent.

.card:has(:hover) {
  outline: 2px solid var(--accent);
}

Hovering any child of .card outlines the whole card. Useful for product grids and dashboard tiles.

6. Sibling-aware paragraph styling.

p:has(+ blockquote) {
  margin-bottom: 0;
}

A paragraph immediately followed by a blockquote loses its bottom margin. No utility class needed.

7. Layout adapts to child count (a lightweight container-query alternative).

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

When the grid has at least four children, switch to two columns.

8. Theme toggle with a hidden checkbox.

body:has(#dark-mode-toggle:checked) {
  --bg: #111;
  --fg: #eee;
}

A single checkbox flips your custom properties. JavaScript only needed for persistence across reloads.

9. Form card with a live focus border.

.form-card:has(:focus-visible) {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(0, 100, 255, 0.15);
}

The card lifts when any field inside is focused.

10. Disable a label when its checkbox is disabled.

label:has(input:disabled) {
  color: var(--muted);
  cursor: not-allowed;
}

Browser support in 2026

By April 2026:

For consumer sites, that is small enough to ship :has() without a fallback. For large or regulated audiences, fall back with a feature query:

.card { padding: 1rem; } /* fallback */

@supports selector(:has(img)) {
  .card:has(img) { padding: 0; }
}

Performance: what to avoid

:has() is fast in modern browsers, but the engine has to evaluate descendants when the DOM changes inside the selector scope. Three things to watch:

Profile in DevTools before assuming a regression. The vast majority of :has() use cases have zero measurable cost.

Tailwind has-[] variant

Tailwind v3.4 (December 2023) added a built-in has-[] variant. v4 keeps it and adds group-has-[] and peer-has-[] for parent and sibling cases.

<!-- Card with image gets zero padding -->
<div class="p-4 has-[img]:p-0">...</div>

<!-- Form with invalid field grays the submit button -->
<form class="group/form">
  ...
  <button class="opacity-100 group-has-[input:invalid]/form:opacity-50">
    Submit
  </button>
</form>

<!-- Sibling-aware spacing -->
<p class="mb-4 has-[+blockquote]:mb-0">...</p>

The square-bracket arbitrary-value syntax accepts any selector inside :has(). No plugin needed.

What :has() replaced

Patterns that used to need JavaScript and now do not:

Each one used to involve a MutationObserver, a state class, and a chunk of JS. Now it is a CSS rule.

FAQ

Is :has() supported everywhere?

As of April 2026, all current Chrome, Firefox, Safari, and Edge versions support it. Around 7 to 9% of global users are on older browsers without support. For consumer apps, ship it. For regulated or enterprise audiences, use @supports selector(:has(...)).

Does :has() impact performance?

For typical use, no measurable impact. For pages with frequent DOM mutations or selectors scoped to body, profile in DevTools before drawing conclusions.

:has() vs JavaScript?

Use :has() for declarative styling that follows DOM state. Use JavaScript when you need to react with logic (analytics, network calls, complex animations) or when the relationship is not a DOM one.

Tailwind support?

Yes, since v3.4. Use has-[] arbitrary-value syntax with any CSS selector inside.

Can I use :has() with multiple selectors?

Yes, in two ways. Comma-separated inside one :has() means "any of": :has(img, video). Chained :has() calls mean AND: :has(input):has(button).

:has() inside :is() or :where()?

Limited. The CSS spec restricts which contexts allow :has(). Most browsers reject :is(:has(...)) silently. Test in your target browsers.

Where to go from here

For more CSS deep-dives, the generators catalog covers gradients, glassmorphism, mesh gradients, neumorphism, and other modern effects you can pair with :has() for state-driven UIs. The CSS to Tailwind converter speeds up moving existing CSS into Tailwind classes, and the Flexbox and CSS Grid playgrounds give you a sandbox for testing layouts.

If you are building out a frontend skill set, the frontend developer roadmap and coding beginner roadmap cover the surrounding CSS, accessibility, and JavaScript fundamentals. For the next layer up, the full-stack developer roadmap ties it into backend and tooling.

Last updated: April 2026.

Last updated: 2026-05-10

Explore more on Talos.tools