The :has() Selector
The Problem
CSS has never had a way to select a parent element based on its children. Developers have relied on JavaScript to toggle classes for parent-child state relationships, such as highlighting a form group when its input is invalid, or changing a card layout based on whether it contains an image. AI agents almost never use :has() and instead suggest JavaScript-based solutions for these patterns.
The Solution
The :has() relational pseudo-class selects elements that contain at least one element matching the given selector list. It acts as a "parent selector" but is far more powerful: it can look at any relative position (children, siblings, descendants) to conditionally apply styles.
Code Examples
Basic Parent Selection
/* Style a card differently when it contains an image */
.card:has(img) {
grid-template-rows: 200px 1fr;
}
.card:has(img) .card-body {
padding-top: 0;
}Form Validation Styling
Style form groups based on input validity without JavaScript.
/* Highlight the entire field group when input is invalid */
.field-group:has(:user-invalid) {
border-left: 3px solid red;
background: #fff5f5;
}
.field-group:has(:user-invalid) .error-message {
display: block;
}
/* Style label when its sibling input is focused */
.field-group:has(input:focus) label {
color: blue;
font-weight: bold;
}<div class="field-group">
<label for="email">Email</label>
<input type="email" id="email" required />
<span class="error-message">Please enter a valid email</span>
</div>Quantity Queries
Adapt layout based on the number of children, with no JavaScript required.
/* Switch to grid layout when a list has 5 or more items */
.item-list:has(> :nth-child(5)) {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
/* Single-column layout for fewer items */
.item-list:not(:has(> :nth-child(5))) {
display: flex;
flex-direction: column;
}/* Style based on even/odd number of children */
.grid:has(> :last-child:nth-child(even)) {
/* Even number of children */
grid-template-columns: repeat(2, 1fr);
}
.grid:has(> :last-child:nth-child(odd)) {
/* Odd number of children */
grid-template-columns: repeat(3, 1fr);
}Styling Based on Sibling State
/* Change page layout when a sidebar checkbox is checked */
body:has(#sidebar-toggle:checked) .main-content {
margin-left: 0;
}
body:has(#sidebar-toggle:checked) .sidebar {
transform: translateX(-100%);
}Combining with Other Selectors
/* Style a navigation item that contains the current page link */
nav li:has(> a[aria-current="page"]) {
background: #e0e7ff;
border-radius: 4px;
}
/* Style a table row that has an empty cell */
tr:has(td:empty) {
opacity: 0.6;
}Using :has() with Direct Child Combinator
Use the direct child combinator > for better performance. It limits the browser's search to immediate children instead of all descendants.
/* Preferred: direct child (faster) */
.container:has(> .alert) {
border: 2px solid red;
}
/* Avoid when possible: descendant (slower on large DOMs) */
.container:has(.alert) {
border: 2px solid red;
}Pitfall: Layout Gated on Unrelated Structure
:has() makes it easy to condition any rule on the presence of any element. Applied to layout rules that must hold unconditionally — responsive stacking, minimum-size resets, overflow guards — this produces pages that break only when the gating element is absent.
The Failing Pattern
/* BROKEN: base layout gated on an unrelated element's presence */
@media (max-width: 1023px) {
.shell:has(#code-panel) {
flex-direction: column;
}
.shell:has(#code-panel) .sidebar {
width: 100%;
}
}The intent was "stack the shell on narrow viewports." The :has(#code-panel) gate ties that intent to an unrelated condition: whether the page happens to contain a code panel. Pages without one — an index page, a settings page — keep row direction on narrow viewports, and the non-shrinking sidebar pushes the main content off-screen. Every page that has the panel renders correctly, so the bug ships silently.
The Fix
Keep unconditional layout unconditional. Gate only the delta the feature introduces:
@media (max-width: 1023px) {
/* Unconditional: every page stacks */
.shell {
flex-direction: column;
}
.shell .sidebar {
width: 100%;
}
/* The panel's own delta needs no gate — this selector
already matches only when the panel exists */
.code-panel {
max-height: 40vh;
overflow-y: auto;
}
/* Gate only deltas on OTHER elements that genuinely
depend on the panel's presence */
.shell:has(#code-panel) .sidebar {
max-height: 120px;
overflow-y: auto;
}
}The stacking now applies to every page. A delta on the panel itself needs no gate — .code-panel can only match when the panel exists. The :has() gate remains only for deltas on other elements that genuinely depend on the panel's presence.
The Smell
A :has(#feature) gate whose declarations style other elements — the sidebar, the main column — deserves scrutiny. The selector shape alone is not the problem: .card:has(img) .card-body from the examples above styles another element correctly, because its delta only makes sense while the image exists. The deciding test is whether the declaration remains necessary when the feature is absent. Responsive stacking, min-size resets, and overflow guards must hold on every page — when a gated declaration passes that test, remove the gate.
Why It Slips Through Review
Every page the author and reviewer open during development happens to contain the feature, so the gated rule fires on every screen they check. The breakage lives only on the feature-absent pages nobody retests at a narrow viewport. Whenever a :has() gate appears in layout code, test the feature-absent variant explicitly.
Browser Support
Chrome 105+
Safari 15.4+
Firefox 121+
Edge 105+
Global support exceeds 96%. Feature detection is available with @supports selector(:has(*)).
Common AI Mistakes
Suggesting JavaScript class toggling when
:has()solves the problem in pure CSSNot knowing
:has()exists and recommending workaroundsUsing descendant selectors inside
:has()when direct child>would be more performantNot combining
:has()with:not()for inverse logic (e.g.,.card:not(:has(img)))Forgetting that
:has()can look at siblings, not just descendants (e.g.,h2:has(+ p))Attempting to polyfill
:has()— it requires real-time DOM awareness and cannot be efficiently polyfilledGating unconditional layout rules (responsive stacking, size resets) on the presence of an unrelated element — see "Pitfall: Layout Gated on Unrelated Structure" above
When to Use
Parent styling based on child state (form validation, content-aware layouts)
Quantity queries to adapt layout based on number of children
State-driven styling without JavaScript (checkbox hacks, focus management)
Conditional component styling based on content presence