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:
:has() does not match the descendant. It matches the element that contains the descendant.
:has() can be nested. .parent:has(.child:has(.grandchild)) is legal.
:has() cannot cross Shadow DOM boundaries.
Some pseudo-class containers (such as :is(:has(...))) are restricted by the spec; test before relying on them.
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:
Chrome 105+ (August 2022): full support.
Edge 105+: full support (Chromium).
Safari 15.4+ (March 2022): full support.
Firefox 121+ (December 2023): full support.
Global coverage: roughly 91% per caniuse, with the remainder mostly old Android WebViews and long-tail enterprise browsers.
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:
Wide scopes. body:has(.error) forces the engine to track every element on the page. Scope tighter; .form:has(.error) is fine.
Deep nesting. :has(:has(:has())) compounds work. Two levels are fine; three or more should be measured.
High DOM-mutation rate. Pages that swap many nodes per second (rich editors, live dashboards) feel :has() costs more than static pages. Modern browsers cache :has() results aggressively, but the cache invalidates on relevant mutations.
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:
Detecting empty lists or containers and showing a placeholder.
Reacting to form validity in real time.
Highlighting a card when any child is hovered.
Dimming a row when its checkbox is unchecked.
"Has 3 or more children" layout switches.
Disabling a submit button while errors exist.
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