CSS :has() Selector in Practice: The Parent Selector That Changes Component Styling Architecture

前端工程(Updated Jun 9, 2026)

The :has() Selector: CSS's "Parent Selector"

For decades, CSS could only select from parent to child. :has() breaks that limitation, enabling child-to-parent reverse selection:

/* Traditional: only select descendants */
.card .title { color: blue; }

/* :has(): select parent based on child state */
.card:has(.title) { border-color: blue; }

Browser Support

Browser Supported Since Release
Chrome 105+ 2022.08
Safari 15.4+ 2022.03
Firefox 121+ 2023.12
Edge 105+ 2022.08

As of 2026, all major browsers support :has().


Syntax and Matching Logic

Basic Syntax

/* Contains specified descendant */
parent:has(descendant) { }

/* Contains direct child */
parent:has(> child) { }

/* Sibling element exists */
element:has(+ sibling) { }
element:has(~ sibling) { }

/* Multiple conditions */
element:has(.a, .b) { }    /* OR */
element:has(.a):has(.b) { } /* AND */

Combining with :not()

/* .nav without an .active child */
.nav:not(:has(.active)) {
  opacity: 0.5;
}

/* Contains an unchecked checkbox */
.form:has(input:not(:checked)) {
  border-color: orange;
}

Practice: Form Validation States

Traditional JS Approach

// Listen to each input's validation state
input.addEventListener('invalid', () => {
  form.classList.add('has-error');
});
input.addEventListener('valid', () => {
  form.classList.remove('has-error');
});

:has() Approach

/* Form with invalid inputs changes overall style */
form:has(:user-invalid) {
  border-color: #ef4444;
}

form:has(:user-invalid) .submit-btn {
  opacity: 0.5;
  pointer-events: none;
}

/* Individual invalid field highlights its container */
.field:has(:user-invalid) {
  background: #fef2f2;
}

.field:has(:user-invalid) .error-msg {
  display: block;
}

/* All valid: show success message */
form:has(.field:not(:has(:user-invalid))) .success-msg {
  display: block;
}
<form>
  <div class="field">
    <label>Email</label>
    <input type="email" required />
    <span class="error-msg">Please enter a valid email</span>
  </div>
  <div class="field">
    <label>Password</label>
    <input type="password" required minlength="8" />
    <span class="error-msg">Password must be at least 8 characters</span>
  </div>
  <button class="submit-btn">Submit</button>
  <span class="success-msg">Form is valid</span>
</form>

Practice: Card Layouts

Adaptive Cards With and Without Images

/* Cards with images: horizontal layout */
.card:has(img) {
  display: flex;
  gap: 1rem;
}

.card:has(img) .card-body {
  flex: 1;
}

/* Cards without images: vertical layout */
.card:not(:has(img)) {
  display: block;
  text-align: center;
}

/* Landscape image: adjust ratio */
.card:has(img[width="1200"]) {
  flex-direction: column;
}

Card Hover Effects

/* Hover anywhere on card: image scales */
.card:has(.card-link:hover) img {
  transform: scale(1.05);
}

.card:has(.card-link:hover) .card-title {
  color: var(--primary);
}

Practice: Navigation Highlighting

Traditional: Server-side active class

<nav>
  <a href="/" class="active">Home</a>
  <a href="/blog">Blog</a>
</nav>

:has() Approach: Pure CSS Page Awareness

/* Using aria-current attribute */
nav:has([aria-current="page"]) {
  border-bottom-color: var(--primary);
}

nav a:has(+ [aria-current="page"]),
nav a[aria-current="page"] {
  color: var(--primary);
  font-weight: 600;
}

/* Show back button only when nav has active item */
nav:not(:has([aria-current="page"])) .back-btn {
  display: none;
}
/* Sidebar with open submenu: show divider */
.sidebar:has(.submenu:not([hidden])) {
  border-right: 2px solid var(--border);
}

/* All submenus collapsed: compact mode */
.sidebar:not(:has(.submenu:not([hidden]))) {
  width: 64px;
}

Practice: Dark Mode Coordination

Auto-adjust Based on Image Brightness

/* Bright image: darken surrounding text */
figure:has(img.light) {
  background: #1a1a1a;
  color: #fff;
}

/* Dark mode: add background behind transparent logo */
@media (prefers-color-scheme: dark) {
  .container:has(img[alt*="logo"]) {
    background: #fff;
    padding: 8px;
    border-radius: 8px;
  }
}

Practice: Table and List Interactions

Table Select-All Awareness

/* All checkboxes checked: header style changes */
table:has(tbody input:checked:not(:indeterminate):not(:only-child))
  th {
  background: var(--primary-light);
}

/* Some checked: show batch actions */
table:has(tbody input:checked) .batch-actions {
  display: flex;
}

/* None checked: hide */
table:not(:has(tbody input:checked)) .batch-actions {
  display: none;
}

List Empty State

/* Empty list shows placeholder */
.list:has(> :only-child.empty-placeholder) {
  justify-content: center;
  min-height: 200px;
}

:has() vs JavaScript: When to Use What

Scenario :has() Advantage JS Advantage
DOM structure changes Auto-responds, no listeners Handles async data
Performance Native, fast style recalc Fine-grained control of timing
Complex logic Simple conditions Multi-step computation, API calls
Compatibility Full support since 2023 No compatibility concerns

Performance Considerations

/* Bad: deeply nested :has() can hurt performance */
.container:has(.deep1:has(.deep2:has(.deep3))) { }

/* Good: flat :has() conditions */
.container:has(.deep3) { }

Avoid deeply nested :has(). The browser must traverse the DOM tree to match — deeper nesting means more overhead.


Combining with Other CSS4 Features

:is() and :where() for Simpler Selectors

/* :is() simplifies multi-type checks */
.card:has(:is(img, video, svg)) {
  aspect-ratio: 16/9;
}

/* :where() lowers specificity for easy overrides */
.card:where(:has(.featured)) {
  border-color: gold;
}
/* Open modal: prevent background scrolling */
body:has(:modal) {
  overflow: hidden;
}

body:has([popover]:not(:popover-closed)) {
  overflow: hidden;
}

Best Practices

  • Prefer :has() over JS class toggling: Less JS, co-located style logic
  • Keep :has() conditions flat: Avoid deep nesting for performance
  • Pair with semantic attributes: aria-current, :user-invalid, :checked
  • Progressive enhancement: Core functionality shouldn't depend on :has()
  • Debug with DevTools: Chrome DevTools supports :has() selector highlighting

Summary

The :has() selector is one of the most significant CSS capability expansions in history, giving CSS child-to-parent reverse selection and drastically reducing JavaScript for form validation, conditional styling, and state coordination. Understanding :has() matching logic and performance boundaries is essential for modern frontend engineers.

Use the Flexbox tool to practice layout combinations, the Box Shadow Generator for card shadow effects, and the Border Radius tool for card corner design.

Try these browser-local tools — no sign-up required →

#:has()#CSS选择器#父选择器#条件样式#CSS4