CSS :has(): The Game Changer for Layouts
AI generated
CSS :has() · Parent Selector · Conditional Styling · 2026
CSS :has(): The Game Changer for Layouts
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.

12 min read :has() · Parent Selector · Conditional Styling · Form States · Browser Support Chrome 105+ · Firefox 121+ · Safari 15.4+

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;
}

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?
A relational pseudo-class selector, it selects an element if it contains certain descendants or siblings. The long missing parent selector in CSS.
2Which browsers support :has()?
Chrome/Edge 105+, Safari 15.4+, Firefox 121+. Over 94% global coverage in 2026. Production ready.
3Can :has() replace JS for form states?
For visual form styling: yes. 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?
Yes. body:has(dialog[open]) { overflow: hidden }, lock body scrolling without JavaScript. A classic :has() application.
7What is :invalid:not(:placeholder-shown)?
Shows validation errors only after user interaction. :placeholder-shown signals an empty field, the combination prevents an immediate error display on load.
8:has() as a container query replacement?
Limited: :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?
No concerns for typical use cases. Very complex selectors with deep traversals can slow down style recalculation, keep it pragmatic.
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.