CSS :has() Selector: The Parent Selector CSS Always Needed
The Parent Selector Problem
For over 20 years, CSS could only select elements based on their ancestors, siblings, or children. You could style a child based on its parent, but you could never style a parent based on what children it contained. This was the single most requested feature in CSS history, and the :has() pseudo-class finally solves it.
The :has() selector matches an element if any of the relative selectors passed as arguments match at least one element when anchored against this element. In simpler terms: it selects a parent based on its children, or an element based on what follows it.
As of 2026, :has() is supported in all major browsers (Chrome 105+, Safari 15.4+, Firefox 121+, Edge 105+), with over 95% global coverage. It is production-ready.
Basic Syntax
The :has() pseudo-class accepts one or more relative selectors as arguments. The element matching :has() is the subject of the selector, not the element inside the parentheses.
/* Select any <a> that contains an <img> */
a:has(img) {
display: inline-block;
border: 2px solid transparent;
}
/* Select any <section> that has an <h2> direct child */
section:has(> h2) {
padding-top: 2rem;
}
/* Select any <div> that contains a checked checkbox */
div:has(input[type="checkbox"]:checked) {
background-color: #e8f5e9;
}
/* Multiple selectors (OR logic) — matches if ANY selector matches */
article:has(img, video) {
grid-column: span 2;
}
/* Chained :has() (AND logic) — must match ALL conditions */
form:has(input:invalid):has(button[type="submit"]) {
border-left: 3px solid red;
}The key insight is that a:has(img) selects the <a> element, not the <img>. The selector in the parentheses defines the condition, but the styled element is the one before :has().
Real-World Pattern: Form Validation Styling
One of the most powerful uses of :has() is styling form groups based on the state of their inputs. Previously, this required JavaScript to add classes to parent elements. Now it is pure CSS.
/* Style the form group when its input is focused */
.form-group:has(input:focus) {
border-color: #6366f1;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
/* Style the form group when its input is invalid */
.form-group:has(input:invalid:not(:placeholder-shown)) {
border-color: #ef4444;
}
/* Style the label when its sibling input has a value */
.form-group:has(input:not(:placeholder-shown)) label {
transform: translateY(-1.5rem) scale(0.85);
color: #6366f1;
}
/* Disable the submit button when any required field is empty */
form:has(input:required:placeholder-shown) button[type="submit"] {
opacity: 0.5;
pointer-events: none;
}
/* Show success message only when all fields are valid */
form:not(:has(input:invalid)) .success-message {
display: block;
}This eliminates dozens of lines of JavaScript event listeners that were previously needed to toggle CSS classes on parent elements. The result is simpler code, fewer bugs, and better performance. For testing these CSS patterns, paste your rules into our CSS Formatter to ensure proper syntax.
Pattern: Conditional Layouts
:has() enables layout changes based on content, not viewport size. This is a paradigm shift from traditional responsive design.
/* Card with image: horizontal layout. Without: vertical. */
.card:has(img) {
display: grid;
grid-template-columns: 200px 1fr;
gap: 1rem;
}
.card:not(:has(img)) {
display: flex;
flex-direction: column;
}
/* Section with sidebar: two-column layout */
.content:has(aside) {
display: grid;
grid-template-columns: 1fr 300px;
gap: 2rem;
}
.content:not(:has(aside)) {
max-width: 65ch;
}
/* Navigation changes based on number of items */
nav:has(> a:nth-child(6)) {
/* More than 5 links: use hamburger-style layout */
flex-wrap: wrap;
}
nav:not(:has(> a:nth-child(6))) {
/* 5 or fewer links: horizontal bar */
display: flex;
gap: 1rem;
}These content-aware layouts were previously impossible without JavaScript. Combined with container queries, :has() makes CSS a truly powerful layout engine. For a comparison of layout systems, see our CSS Grid vs Flexbox guide.
Pattern: Previous Sibling Selection
CSS has always had the adjacent sibling combinator (+) and general sibling combinator (~), but they only select following siblings. With :has(), you can finally select previous siblings.
/* Select the element BEFORE a hovered element */
li:has(+ li:hover) {
/* This selects the <li> immediately before the hovered <li> */
opacity: 0.7;
}
/* Highlight the row above a selected row in a table */
tr:has(+ tr.selected) {
border-bottom: 2px solid #6366f1;
}
/* Style all siblings before a certain element */
li:has(~ li.active) {
/* Selects all <li> elements that have an .active sibling after them */
color: #22c55e;
}
/* Combined: highlight neighbors of hovered item */
li:has(+ li:hover), /* previous sibling */
li:hover, /* the item itself */
li:hover + li { /* next sibling */
transform: scale(1.05);
background: rgba(99, 102, 241, 0.1);
}The "previous sibling" pattern opens up interaction designs that were only possible with JavaScript. Carousel indicators, breadcrumb trails, and progress bars can now be styled purely with CSS.
Pattern: Theming and Dark Mode
/* Apply dark theme when a toggle checkbox is checked */
body:has(#dark-mode-toggle:checked) {
--bg: #1a1a2e;
--text: #e0e0e0;
--accent: #818cf8;
}
/* Apply high-contrast mode */
body:has(#high-contrast-toggle:checked) {
--bg: #000;
--text: #fff;
--accent: #ffff00;
--border: #fff;
}
/* Conditional component styles based on page context */
body:has(.hero-banner) header {
/* Transparent header when hero banner is present */
background: transparent;
position: absolute;
}
body:not(:has(.hero-banner)) header {
/* Solid header on pages without a hero */
background: var(--bg);
position: sticky;
top: 0;
}This approach lets users toggle themes without any JavaScript state management. The checkbox state drives the CSS variables, and everything updates reactively. Use our Color Converter to generate matching color values for your theme variables.
Combining :has() with Other Modern CSS
:has() becomes even more powerful when combined with container queries, :is(), :where(), and @layer.
/* :has() + :is() — match multiple child types efficiently */
.card:has(:is(img, video, canvas)) {
aspect-ratio: 16 / 9;
overflow: hidden;
}
/* :has() + :where() — low specificity condition */
:where(.card):has(img) {
/* Easy to override because :where() has zero specificity */
border: 1px solid #333;
}
/* :has() + container queries — responsive + content-aware */
.card-container {
container-type: inline-size;
}
@container (min-width: 400px) {
.card:has(img) {
grid-template-columns: 150px 1fr;
}
}
/* :has() + :not() — powerful exclusions */
.grid > *:not(:has(img)):not(:has(video)) {
/* Text-only grid items get different styling */
padding: 2rem;
background: var(--surface);
}
/* :has() + nesting (native CSS nesting) */
.form-group {
border: 1px solid #333;
&:has(:focus) {
border-color: #6366f1;
}
&:has(:invalid) {
border-color: #ef4444;
}
}Performance Considerations
The :has() selector requires the browser to evaluate the DOM tree differently from traditional selectors. While browser implementations have been heavily optimized, there are patterns to avoid for best performance.
- Avoid broad :has() on the body --
body:has(.some-class)forces the browser to check the entire DOM tree. Scope your selectors as narrowly as possible. - Use direct child combinators --
.parent:has(> .child)is faster than.parent:has(.child)because it limits the search depth. - Limit nesting depth --
:has(:has(:has(...)))is technically valid but forces deep tree traversal on every style recalculation. - Be cautious with dynamic content -- If elements matching the
:has()condition are frequently added or removed, the browser must recalculate styles more often. - Profile with DevTools -- Use Chrome DevTools Performance panel to check if
:has()selectors are causing excessive "Recalculate Style" events.
In practice, the performance impact is negligible for most websites. The cases where it matters are large DOM trees (10,000+ elements) with frequently changing state. For more on measuring performance, see our Web Performance Checklist.
Progressive Enhancement
While browser support for :has() is excellent in 2026, you may still need to support older browsers. Use @supports for graceful fallbacks.
/* Fallback: JavaScript adds a class */
.form-group.has-focus {
border-color: #6366f1;
}
/* Enhancement: pure CSS with :has() */
@supports selector(:has(*)) {
.form-group:has(:focus) {
border-color: #6366f1;
}
}
/* Feature detection in JavaScript */
const supportsHas = CSS.supports('selector(:has(*))');
if (!supportsHas) {
// Add JS-based class toggling as fallback
document.querySelectorAll('.form-group input').forEach(input => {
input.addEventListener('focus', () => {
input.closest('.form-group').classList.add('has-focus');
});
input.addEventListener('blur', () => {
input.closest('.form-group').classList.remove('has-focus');
});
});
}Common Mistakes to Avoid
- Forgetting that :has() selects the parent --
.card:has(img)styles the.card, not theimg. This is the most common source of confusion. - Using :has() where a child selector works -- If you want to style the child, use a normal descendant selector.
:has()is for when you need to style the ancestor or sibling. - Overusing :has() for simple patterns -- Adding a CSS class in HTML is simpler and more performant than a complex
:has()chain when you control the markup. - Nesting :has() inside :has() -- While valid, deeply nested
:has()selectors are hard to read and can have performance implications. Flatten your logic when possible. - Ignoring specificity --
:has()contributes to specificity based on its most specific argument..card:has(#unique)has very high specificity because of the ID selector inside.
Browser Support Summary
| Browser | Version | Release Date |
|---|---|---|
| Chrome | 105+ | August 2022 |
| Safari | 15.4+ | March 2022 |
| Firefox | 121+ | December 2023 |
| Edge | 105+ | August 2022 |
| Samsung Internet | 20+ | 2023 |
With all major browsers supporting :has() since late 2023, global support exceeds 95% in 2026. It is safe to use without fallbacks for most projects. Test your CSS selectors with our Regex Tester to validate selector syntax patterns.
Format and Validate Your CSS
Writing complex selectors with :has()? Keep your stylesheets organized with our free CSS Formatter. Paste your CSS, auto-indent, and catch syntax issues before they reach production.
Open CSS FormatterRelated Articles
CSS Grid vs Flexbox
When to use each layout system with side-by-side examples.
CSS Flexbox vs Grid (with Examples)
Practical layout recipes for navbars, cards, and dashboards.
Web Performance Checklist 2026
Use modern CSS features to reduce JavaScript overhead.
Web Accessibility Testing
CSS :has() enables better accessible form patterns.