// blog/developer/
Back to Blog
Developer · Published August 24, 2026 · 10 min read · By Toine

CSS :has() Selector: The Parent Selector for CSS

CSS :has() Selector: The Parent Selector for CSS

For decades, CSS could only style elements based on ancestors, siblings, and their own attributes. You could style a child based on its parent, never a parent based on its child. If a form field had an error you could give the input a red border, but the surrounding label or container needed JavaScript.

The :has() pseudo-class changes that. Call it a parent selector if you want, but it is more powerful than that. It lets you pick any element based on what it contains, what follows it, or what state its descendants are in. CSS can now react to DOM structure and state that previously needed JavaScript.

Browser support hit full coverage in late 2023 (Chrome 105+, Safari 15.4+, Firefox 121+). In 2026 it is safe to use in production without fallbacks for almost all users.

* * *

How :has() Works: The Basics

The :has() selector takes a relative selector list as its argument. It matches an element if any of the relative selectors match at least one element when anchored against the original element.

In plain English: "select this element if it has a descendant/sibling that matches."

`css / Style a card that contains an image / .card:has(img) { grid-template-rows: auto 1fr; }

/ Style a card that does NOT contain an image / .card:not(:has(img)) { grid-template-rows: 1fr; }

/ Style a label when its input is focused / label:has(input:focus) { color: #3b82f6; font-weight: 600; }

/ Style a form when any field has an error / form:has(.error) { border: 2px solid #ef4444; } `

The selector reads naturally: ".card that has an img," "label that has a focused input," "form that has a .error element." This readability is one of its best features.

You can combine :has() with any other selector, including other pseudo-classes, attribute selectors, and combinators. The possibilities are enormous.

After writing your :has() rules, compress the final stylesheet with the CSS Minifier for production deployment.

* * *

Practical Patterns That Replace JavaScript

Many common UI patterns that previously required JavaScript event listeners can now be handled purely in CSS with :has().

Form validation styling: `css / Highlight the entire field group when input is invalid / .field-group:has(input:invalid:not(:placeholder-shown)) { background: #fef2f2; border-left: 3px solid #ef4444; } `

Responsive navigation: `css / Adjust layout when a dropdown is open / nav:has(.dropdown.open) { background: rgba(0, 0, 0, 0.95); } `

Conditional layouts: `css / Full-width layout when sidebar is absent / .page:has(.sidebar) .content { width: 70%; } .page:not(:has(.sidebar)) .content { width: 100%; } `

Empty state styling: `css / Show a message when a list has no items / .list:not(:has(li)) { min-height: 200px; display: grid; place-items: center; } .list:not(:has(li))::after { content: 'No items yet'; color: #9ca3af; } `

Each of these patterns would previously need JavaScript to toggle classes based on DOM state. With :has(), the browser handles it natively, which is faster and eliminates an entire category of bugs.

Prototype layouts that respond to :has() rules with the CSS Flexbox Generator. Easy to sketch wrap, gap, and alignment changes that trigger when specific children appear.

Code editor showing CSS :has() selector examples with syntax highlighting
Code editor showing CSS :has() selector examples with syntax highlighting
* * *

Advanced :has() Techniques

Beyond basic parent selection, :has() enables patterns that were simply impossible in CSS before.

Quantity queries: style elements differently based on how many siblings exist: `css / Grid layout when there are 3+ items / .grid:has(:nth-child(3)) { display: grid; grid-template-columns: repeat(3, 1fr); }

/ Stack layout when fewer than 3 / .grid:not(:has(:nth-child(3))) { display: flex; flex-direction: column; } `

Previous sibling selector: CSS has the next-sibling combinator (+) but never had a previous-sibling selector. :has() solves this: `css / Style the element BEFORE a focused input / .field:has(+ .field input:focus) { opacity: 0.5; } `

Theme switching based on content: `css / Dark section when it contains a dark-themed component / section:has(.hero-dark) { background: #0f172a; color: #e2e8f0; } `

Table row highlighting: `css / Highlight the entire row when a checkbox is checked / tr:has(input[type="checkbox"]:checked) { background: #eff6ff; } `

These techniques are performant in modern browsers. Initial concerns about :has() performance were addressed by browser vendors through optimization of the selector matching engine. For typical use cases, the performance impact is negligible.

* * *

Performance Considerations and Limitations

While :has() is powerful, there are some practical limitations to be aware of.

Cannot be used in @supports properly: testing for :has() support with @supports selector(:has(*)) works in some browsers but not all. The safest approach in 2026 is to use :has() directly, since browser support is universal for modern users.

Specificity: :has() contributes the specificity of its most specific argument. div:has(#important) has the specificity of a div + an ID selector. This can cause unexpected specificity conflicts if you are not careful.

Cannot nest :has() inside :has(): a:has(:has(b)) is invalid. However, you can chain :has() on the same element: a:has(b):has(c) is valid and means "a that has both b and c as descendants."

Performance with large DOM trees: :has() can be expensive on very large pages (10,000+ nodes) with complex selectors. Browsers optimize common patterns, but deeply nested :has() chains with broad selectors (like :has(*)) should be avoided.

Live pseudo-classes work: :has(:hover), :has(:focus), and :has(:checked) all work and update in real-time. This is what makes interactive patterns possible without JavaScript. The browser re-evaluates the selector when the pseudo-class state changes.

Minify your HTML alongside your CSS with the HTML Minifier to ensure the smallest possible payload in production.

Key takeaway

While `:has()` is powerful, there are some practical limitations to be aware of.

* * *

Migrating JavaScript Logic to :has()

If you have existing JavaScript that toggles classes based on child element state, :has() can often replace it.

Before (JavaScript): `javascript document.querySelectorAll('input').forEach(input => { input.addEventListener('focus', () => { input.closest('.field-group').classList.add('focused'); }); input.addEventListener('blur', () => { input.closest('.field-group').classList.remove('focused'); }); }); `

After (CSS only): `css .field-group:has(input:focus) { / focused styles / } `

The CSS version is: - Fewer lines of code - No event listener management - No class toggle bugs - No JavaScript execution cost - Automatically handles dynamically added elements

Not all JavaScript class-toggling can be replaced. :has() only works with CSS-observable state (hover, focus, checked, valid/invalid, presence of elements). Business logic that determines whether to show or hide content (like authentication checks or API responses) still needs JavaScript.

The migration approach: search your JavaScript for classList.add, classList.toggle, and closest() calls. For each one, check whether the trigger is a CSS-observable state. If yes, replace with :has(). If no, keep the JavaScript.

Web page layout demonstrating dynamic styling changes triggered by :has() selectors
Web page layout demonstrating dynamic styling changes triggered by :has() selectors
* * *

FAQ

Is :has() supported in all major browsers?

Yes, as of 2026. Chrome 105+ (August 2022), Safari 15.4+ (March 2022), Firefox 121+ (December 2023), and Edge 105+. The only browsers that lack support are Internet Explorer (discontinued) and very old mobile browsers. For virtually all modern web traffic, :has() is safe to use without fallbacks.

Does :has() affect CSS rendering performance?

For typical usage, the performance impact is negligible. Browser vendors specifically optimized their engines for common :has() patterns. Avoid using :has() with very broad selectors (like :has(*)) on pages with thousands of DOM nodes. Specific selectors like :has(.class-name) or :has(input:checked) are well-optimized.

Can I use :has() with CSS animations and transitions?

Yes. Styles applied via :has() participate in transitions and animations normally. If you transition the background-color of a .card:has(:hover), the transition plays when a child element is hovered. This enables sophisticated interaction effects without JavaScript.

How does :has() interact with shadow DOM?

:has() does not pierce the shadow DOM boundary. A selector like div:has(slot) will match if the div contains a element in the light DOM, but it cannot check the content distributed into the slot from the shadow DOM. This is consistent with how other CSS selectors handle shadow boundaries.

Key takeaway

### Is :has() supported in all major browsers.