Parent selector, conditional styling and form states
CSS :has() is more than a parent selector, it is a complete context selector. Layout decisions that used to require JavaScript event handlers can now be made declaratively right in the stylesheet. That changes how you build CSS architectures.
Table of Contents
- 1. What CSS :has() really is
- 2. Syntax and basic rules
- 3. The parent selector: styling parents based on children
- 4. Conditional styling: layouts based on child elements
- 5. Form states: validation without JavaScript
- 6. Combining :has() with other pseudo-classes
- 7. Navigation and interactive components
- 8. Browser support in 2026
- 9. :has() versus JavaScript solutions compared
- 10. Summary
- 11. FAQ
1. What CSS :has() really is
CSS :has() is often called "the parent selector" that developers have been asking for over the years. That is correct, but it is only half the story. CSS :has() is a relational pseudo-class selector that selects an element if it contains one or more matching descendants. That "match" can be a direct child, any descendant, or even a sibling element in certain combinations. The power lies in the fact that CSS rules can now be applied to an element that sits above or next to the condition element in the DOM, a capability that CSS was completely missing before.
What makes CSS :has() revolutionary is that it flips the direction of selection. Every CSS selector before CSS :has() worked forward: you select an element because it has a certain ancestor, a certain type, or a certain state. CSS :has() lets you select backward: you select an element because its descendants or siblings have a certain state. Does a card with an image need to look different from a card without one? With CSS :has(), one rule is enough: .card:has(img). Should a form with an invalid required field disable its submit button? form:has(:invalid) button[type=submit].
Before CSS :has(), conditions like these required JavaScript: event listeners on state changes, DOM traversal upward, class toggling. Every one of these JavaScript solutions has latency, it reacts to events rather than running in step with the browser's layout process. CSS :has() is part of the style recalculation process and has no latency.
2. Syntax and basic rules
The syntax of CSS :has() is simple: the selector that expresses the condition sits as an argument inside the parentheses. div:has(p) selects every div element that contains at least one p descendant. div:has(> p) selects only div elements with a direct p child. div:has(+ p) selects a div that is followed by a p sibling, the sibling context that CSS :has() makes possible.
Inside CSS :has() you can use any selector, including pseudo-classes and pseudo-elements. form:has(input:focus) selects a form that contains a focused input. .menu:has(li:hover) selects a menu in which a list item is currently being hovered. article:has(h2 + p) selects articles in which an h2 is directly followed by a p. Selectors inside CSS :has() are relative to the selected element, they start the matching process as if :scope were prepended.
/* CSS :has(): practical patterns */
/* Parent selector: card with image gets different layout */
.card:has(img) {
grid-template-rows: auto 1fr auto; /* separate image row */
}
.card:not(:has(img)) {
padding-top: 2rem; /* extra top space without image */
}
/* Sibling context: label after a focused input */
.field:has(input:focus) label {
color: #7c3aed;
transform: translateY(-0.25rem);
transition: all 0.2s ease;
}
/* State-based layout: form with invalid fields */
form:has(:invalid:not(:placeholder-shown)) .form-hint {
display: block; /* show hint only when user has interacted */
}
/* Container query alternative: section with many items */
.grid-container:has(:nth-child(n+7)) {
grid-template-columns: repeat(4, 1fr); /* switch to 4 cols at 7+ items */
}
.grid-container:not(:has(:nth-child(n+7))) {
grid-template-columns: repeat(3, 1fr);
}
/* Interactive navigation: highlight parent of active item */
nav:has(.nav-item.active) {
border-bottom: 2px solid #7c3aed;
}
3. The parent selector: styling parents based on children
The most commonly cited use case for CSS :has() is the parent selector: an element gets styled based on its child elements. The simplest example: a list item that should look selected once its hidden checkbox child is checked. li:has(input[type=checkbox]:checked) selects exactly those items, no JavaScript, no class toggling, no event handler. The checkbox element can be visually hidden, its state is still visible to CSS :has().
A more advanced example: an accordion component whose title is styled differently depending on whether its content is visible or collapsed. Using a details element together with CSS :has(), this can be built without JavaScript: details:has(> summary + *) checks whether a details element contains more than just a summary, which is always true, but combined with details[open]:has(> summary) you can style the open and closed states differently. CSS :has() makes the HTML semantics usable for styling.
Even more directly: label:has(+ input:required) selects a label that is followed by a required field. That lets you automatically mark required field labels with an asterisk via CSS ::after, purely declaratively, with no HTML changes and no JavaScript. That is the philosophical core of CSS :has(): CSS can now answer questions about the state of its surrounding HTML context and react to it directly.
4. Conditional styling: layouts based on child elements
CSS :has() enables genuine conditional styling at the CSS level, layout decisions based on actual content rather than manually applied classes. Card components are a great example: a card with an image needs a different layout than a card without one. With CSS :has() that can be expressed directly, without the server or JavaScript having to set a class at render time.
Content blocks in CMS systems behave similarly: an article element that contains a figure gets a float layout. One without a figure stays in a linear text flow. With CSS :has() this conditional styling approach can be expressed right in the stylesheet: article:has(figure) { display: grid; grid-template-columns: 1fr 1fr; } and article:not(:has(figure)) { max-width: 65ch; }. The CMS simply outputs semantic HTML, and CSS takes care of every layout decision.
CSS :has() becomes especially powerful as an alternative to container queries for content-sensitive layouts: .card-grid:has(:nth-child(n+5)) can change the grid layout once more than four cards are present. That is not a true container query replacement, but for certain discrete state changes, few versus many items, CSS :has() is more direct and does not need an additional CSS feature.
/* Conditional Styling with CSS :has(), no JavaScript needed */
/* Card layout adapts based on content presence */
.card { display: flex; flex-direction: column; gap: 1rem; padding: 1.5rem; }
.card:has(.card__image) {
padding: 0; /* image-first: no top padding, image touches border */
overflow: hidden;
}
.card:has(.card__image) .card__body {
padding: 1.25rem 1.5rem 1.5rem;
}
/* Article: grid layout only when figure is present */
.article-body:has(figure) {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
align-items: start;
}
.article-body:has(figure) figure {
grid-column: 2;
grid-row: 1 / span 3;
position: sticky;
top: 1rem;
}
/* Highlight required labels automatically via :has() */
.field:has(input[required]) > label::after,
.field:has(select[required]) > label::after {
content: " *";
color: #dc2626;
font-weight: 700;
}
/* Section with overflow: show scroll indicator */
.content-box:has(.inner:overflow) {
position: relative;
}
.content-box:has(> *:nth-child(n+4)) .show-more {
display: block; /* reveal "show more" link if 4+ children exist */
}
5. Form states: validation without JavaScript
Forms are the area where CSS :has() delivers the biggest immediate productivity gain. Combining CSS :has() with form pseudo-classes such as :invalid, :valid, :required, :optional, :focus, :placeholder-shown and :checked enables declarative form UX that used to be exclusively JavaScript territory. A submit button that looks disabled as long as the form contains invalid required fields: form:has(:invalid) [type=submit].
The pattern :invalid:not(:placeholder-shown) is an important refinement: :invalid alone is active on required fields the instant the form loads, even before the user has typed anything. :placeholder-shown is active while the placeholder is visible, in other words while the field is still empty. Combining them as :invalid:not(:placeholder-shown) shows an error only once the user has actually entered something invalid. This is expressible with CSS :has() on the container: form:has(:invalid:not(:placeholder-shown)) selects the form in exactly the state where error feedback is appropriate.
6. Combining :has() with other pseudo-classes
CSS :has() shows its full strength in combination with other pseudo-classes and pseudo-elements. :is() and :where() inside CSS :has() allow complex selection logic with multiple conditions. section:has(:is(h2, h3)) selects sections that contain either an h2 or an h3. :not(:has(img)) inverts the condition and selects elements that contain no image. These logical combinations turn CSS :has() into a complete conditional system inside the stylesheet.
Combining CSS :has() with :hover, :focus-within and :active opens up new interactive styling patterns. .card:has(.card__cta:hover) highlights the entire card when the CTA button is hovered, with no JavaScript and no complex selectors that would need to navigate backward through the DOM. nav:has(a:focus-visible) can highlight an entire navigation element while a keyboard user is navigating through it.
/* CSS :has() with form validation pseudo-classes */
/* Submit button: visually disabled when form has invalid fields */
form:has(:invalid:not(:placeholder-shown)) [type="submit"] {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
/* Field error: show error message only after user interaction */
.field:has(input:invalid:not(:placeholder-shown)) .field__error {
display: block;
color: #dc2626;
font-size: 0.75rem;
margin-top: 0.25rem;
}
/* Field success: green border when valid and filled */
.field:has(input:valid:not(:placeholder-shown)) input {
border-color: #16a34a;
outline-color: #16a34a;
}
/* Show password requirements only when password field is focused */
form:has(input[type="password"]:focus) .password-requirements {
display: block;
animation: fadeIn 0.2s ease;
}
/* Highlight entire card on CTA hover, no JS needed */
.card:has(.card__cta:hover) {
box-shadow: 0 8px 30px rgba(124, 58, 237, 0.2);
transform: translateY(-2px);
transition: all 0.2s ease;
}
/* Auto-mark required labels */
.form-field:has(input[required]) label::after {
content: " *";
color: #dc2626;
}
7. Navigation and interactive components
CSS :has() changes how you build interactive navigation components. A dropdown menu that highlights its parent link when one of its submenu items is active: .nav-item:has(.submenu-item.active). A mega menu that highlights the main navigation area when anything inside it is hovered: .main-nav:has(a:hover). These selectors used to be reachable only via JavaScript that traversed the DOM upward and set classes.
CSS :has() is especially useful for state-dependent layouts in single-page applications. When a modal is open, body scrolling should be locked: previously JavaScript would set overflow: hidden on the body. With CSS :has() this can be expressed purely in CSS: body:has(dialog[open]) selects the body when an open dialog element is present. body:has(.drawer.open) locks scrolling when a sidebar is open. State management stays in the HTML, the visual feedback stays in the CSS.
8. Browser support in 2026
CSS :has() has achieved very solid browser support by 2026. Chrome and Edge have supported CSS :has() since version 105 (August 2022), Safari since version 15.4 (March 2022), Firefox since version 121 (December 2023). Global usage coverage is above 94% in 2026. Internet Explorer is not supported, which was expected. The only relevant browser that implemented CSS :has() late was Firefox, but since version 121 that objection is obsolete too.
For progressive enhancement, @supports selector(:has(a)) is recommended. It lets you define a baseline layout for older browsers and add the CSS :has()-enhanced version for modern browsers. In most projects this precaution has become optional by 2026, browser coverage is sufficient for production use without a fallback. Unlike some other modern CSS features, CSS :has() no longer has browser bugs in current versions that would limit production use.
9. :has() versus JavaScript solutions compared
A direct comparison between CSS :has() and JavaScript-based solutions reveals significant differences in complexity, performance and maintainability. For every use case that CSS :has() covers, the pure CSS solution is preferable, it is synchronous with the browser's rendering process, needs no event listener lifecycle, and can be expressed in a single CSS rule.
| Use Case | JavaScript Solution | CSS :has() Solution | Advantage |
|---|---|---|---|
| Highlight parent on child hover | mouseover + classList.add on parent | .parent:has(.child:hover) |
No event listeners, no latency |
| Form state | input event + validation + class toggle | form:has(:invalid) |
Synchronous, no JS bundle |
| Layout based on content | DOM check + conditional classList | .card:has(img) |
No render blocking, SSR compatible |
| Lock body scroll | document.body.style.overflow = 'hidden' | body:has(dialog[open]) |
State lives in HTML, no JS state management |
| Mark active parent nav item | DOM traversal + parentElement.classList | .nav-item:has(.active) |
Readable, maintainable, zero JS |
The CSS :has() solutions are shorter, more readable and more maintainable in every case. The only remaining area where JavaScript stays necessary is genuine data and logic operations that go beyond CSS selectors: network requests, complex state management logic, dynamically computed values. Pure styling reactions to DOM states can be fully expressed in the stylesheet with CSS :has().
Mironsoft
Modern CSS, interactive components and JS-free UI solutions
Want to put CSS :has() to work in your components?
We turn JavaScript-based styling logic into declarative CSS using :has(), :is(), container queries and modern pseudo-classes, for a smaller JS bundle, faster load times and a more maintainable codebase.
CSS Audit
Identifying JS styling logic that can be moved into CSS using :has()
Form UX
Validation, states and conditional feedback with modern CSS features
Components
Content-adaptive cards, navigation and interactive UI elements without JS
10. Summary
CSS :has() is the most powerful new CSS feature of recent years. As a relational pseudo-class selector, CSS :has() lets you style elements based on their descendants and siblings, something that used to be possible only with JavaScript. Parent selectors, content-based conditional styling, form state visualization without event handlers, body scroll control based on dialog state: all of it expressible purely declaratively in the stylesheet. Browser support is production ready in 2026, with over 94% global coverage.
The strategic value of CSS :has() lies in rolling back JavaScript dependencies for purely styling reactions to DOM state. Every piece of styling logic moved out of the JavaScript bundle and into CSS improves initialization performance, reduces the complexity of the JavaScript code, and makes the styling behavior visible to designers and frontend developers right in the stylesheet. CSS :has() should be the first choice evaluated in 2026 for any modern project that needs DOM-state-reactive styling.
CSS :has(): the essentials at a glance
Parent Selector
.parent:has(.child), selects the parent element when the child exists or has a certain state.
Form States
form:has(:invalid:not(:placeholder-shown)), error feedback only after user interaction, no JavaScript required.
Browser Support 2026
Chrome 105+, Safari 15.4+, Firefox 121+. Over 94% global coverage. Production ready without a fallback.
Progressive Enhancement
@supports selector(:has(a)) for a fallback on older browsers. Optional in most projects by 2026.
11. FAQ: CSS :has()
1What does CSS :has() do?
2Which browsers support :has()?
3Can :has() replace JS for form states?
form:has(:invalid) replaces classic JS state feedback without an event handler.4Combining :has() with :not()?
.card:not(:has(img)) for cards without an image, .card:has(img) for cards with an image, two layouts, no classes, no JS.5@supports for :has()?
@supports selector(:has(a)), progressive enhancement for older browsers. Optional in most projects by 2026.6Is body:has(dialog[open]) possible?
body:has(dialog[open]) { overflow: hidden }, lock body scrolling without JavaScript. A classic :has() application.7What is :invalid:not(:placeholder-shown)?
:placeholder-shown signals an empty field, the combination prevents an immediate error display on load.8:has() as a container query replacement?
:has(:nth-child(n+5)) can change a layout when more than four children exist. For measurement-based queries, @container remains the right choice.9Performance concerns?
10Sibling context with :has()?
.element:has(+ .sibling) selects an element that is followed by a specific sibling. A new selection context that used to be impossible in CSS.