EdgeCases Logo
Apr 2026
CSS
Deep
9 min read

CSS :has() Advanced Patterns

Beyond basic parent selection with :has(). Learn multi-level selection, performance optimization, browser fallbacks, and dynamic form validation patterns.

css
has-selector
parent-selector
form-validation
performance
edge-case

Beyond basic parent selection, :has() enables complex relational patterns previously impossible in pure CSS. Multi-level parent selection, conditional styling based on sibling states, and dynamic form validation all become possible without JavaScript.

The Problem: Beyond Basic :has()

Most :has() examples show simple parent selection (div:has(.child)). But real-world styling requires understanding:

  • Multi-level parent selection
  • Performance implications of complex selectors
  • Browser fallback strategies
  • Dynamic form validation states

Pattern 1: Multi-Level Parent Selection

Select grandparents or ancestors based on deep descendants:

/* Select article if it contains a heading with a link */
article:has(h2 a) {
  /* Article has a linked heading */
}

/* Select card if it contains an image AND a button */
.card:has(img):has(button) {
  /* Card is actionable content */
}

/* Select section if ANY descendant has error class */
section:has(.error) {
  border-left: 3px solid red;
}

Chain :has() for multiple conditions:

/* Select grid if it has more than 4 items AND has an active item */
.grid:has(> :nth-child(5)):has(.active) {
  /* Grid with 5+ items, one of which is active */
}

Pattern 2: Conditional Sibling Selection

Style elements based on siblings' children:

/* Style label based on its input's validity */
label:has(+ input:invalid) {
  color: red;
}

/* Style button based on previous sibling's state */
button:has(+ .processing) {
  opacity: 0.5;
  pointer-events: none;
}

/* Style div based on following sibling having a class */
.container:has(~ .success) {
  border-color: green;
}

Pattern 3: Dynamic Form Validation

Create rich form validation states without JavaScript:

/* Fieldset with any invalid field */
fieldset:has(:invalid) {
  border-color: var(--error);
  background: var(--error-bg);
}

/* Fieldset when all fields are valid */
fieldset:has(:valid):not(:has(:invalid)) {
  border-color: var(--success);
}

/* Submit button state */
form:has(:invalid) button[type="submit"] {
  opacity: 0.5;
  cursor: not-allowed;
}

form:not(:has(:invalid)) button[type="submit"] {
  opacity: 1;
  cursor: pointer;
}

Combine with :focus-within:

/* Highlight form row when focused */
.form-row:has(:focus-within) {
  background: var(--highlight);
}

/* Show helper text only when input is invalid */
.form-row:has(input:invalid) .helper {
  display: block;
  color: var(--error);
}

/* Hide helper when input is valid */
.form-row:has(input:valid) .helper {
  display: none;
}

Pattern 4: Quantity-Based Styling

Change layouts based on child count:

/* Grid changes columns based on item count */
.grid:has(> :nth-child(1)) {
  grid-template-columns: 1fr;
}

.grid:has(> :nth-child(2)) {
  grid-template-columns: 1fr 1fr;
}

.grid:has(> :nth-child(4)) {
  grid-template-columns: repeat(2, 1fr);
}

.grid:has(> :nth-child(6)) {
  grid-template-columns: repeat(3, 1fr);
}

/* Full width for single item */
.grid:not(:has(> :nth-child(2))) > * {
  grid-column: 1 / -1;
}

Pattern 5: State-Based Styling

React to complex component states:

/* Card when its image is hovered */
.card:has(img:hover) {
  transform: translateY(-2px);
  box-shadow: 0 8px 16px rgba(0,0,0,0.2);
}

/* Dropdown menu when parent li is hovered */
nav li:has(.dropdown:hover) {
  background: var(--nav-active);
}

/* Tooltip when target is focused */
button:has([aria-label]:focus-visible) {
  position: relative;
}

button:has([aria-label]:focus-visible)::after {
  content: attr(aria-label);
  position: absolute;
  bottom: 100%;
  background: var(--tooltip-bg);
  padding: 0.5rem;
}

Edge Case 1: Performance Impact

:has() can be expensive on complex selectors:

/* ❌ Bad: Expensive selector */
*:has(.active) { }

/* This checks every single element on the page */

/* ✅ Good: Scoped selector */
.container:has(.active) { }

/* Only checks elements within .container */

/* ✅ Better: Direct child check */
.container:has(> .active) { }

/* Only checks direct children, not all descendants */

Profile :has() performance:

/* Chrome DevTools > More Tools > Rendering */
/* Enable "Selector Stats" to see expensive selectors */

/* Common performance issues:
- Universal selectors with :has()
- Deep descendant searches
- Complex combinators inside :has() */

Edge Case 2: Browser Fallbacks

:has() is unsupported in older browsers. Use progressive enhancement:

/* Base styles (works everywhere) */
.card {
  border: 1px solid var(--border);
}

/* Progressive enhancement with :has() */
@supports selector(:has(*)) {
  .card:has(.featured) {
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent);
  }
}

/* Alternative: JavaScript fallback */
.no-has-support .card.featured {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent);
}

Feature detection in JavaScript:

// Detect :has() support
function supportsHas() {
  return CSS.supports('selector(:has(*))');
}

if (!supportsHas()) {
  document.documentElement.classList.add('no-has-support');
  // Add :has() polyfill or apply fallback styles
}

Edge Case 3: Specificity Calculation

:has() contributes to specificity based on its most specific argument:

/* Specificity: (0, 1, 1) - element + class */
div:has(.foo) { }

/* Specificity: (1, 0, 1) - element + ID */
div:has(#bar) { }

/* Specificity: (0, 2, 0) - two classes */
:has(.foo.bar) { }

/* Specificity: (0, 2, 1) - element + two classes in :has() */
div:has(.foo.bar) { }

Specificity examples:

/* Lower specificity */
.card:has(img) { }

/* Higher specificity (due to ID in :has()) */
.card:has(#featured-image) { }

/* This wins even though selector looks similar */
.card:has(#featured-image) {
  border: 2px solid gold;
}

Pattern 6: Combinator Chaining

Use combinators inside :has() for complex relationships:

/* Select div if it has a p followed by a button */
div:has(p + button) { }

/* Select section if it has h2 followed by paragraph */
section:has(h2 + p) { }

/* Select list if it has li with class .active */
ul:has(li.active) { }

/* Select grid if it has div with class .card as child */
.grid:has(> .card) { }

Pattern 7: Empty State Handling

Show empty states based on content:

/* List has no visible items */
.list:not(:has(li:not([hidden]))) {
  display: flex;
  justify-content: center;
  min-height: 200px;
}

.list:not(:has(li:not([hidden])))::after {
  content: "No items to display";
  color: var(--muted);
}

/* List has items */
.list:has(li:not([hidden]))::after {
  content: none;
}

/* Show empty state only when list is empty */
.empty-state {
  display: none;
}

.list:not(:has(li)) .empty-state {
  display: flex;
}

Pattern 8: Responsive Design Without Media Queries

Use :has() for container-based responsiveness:

/* Card layout changes based on content */
.card:has(img) {
  display: grid;
  grid-template-columns: 200px 1fr;
  gap: 1rem;
}

.card:not(:has(img)) {
  display: block;
  padding: 1.5rem;
}

/* Navigation changes based on presence of dropdown */
nav:has(.dropdown) {
  position: relative;
}

nav:not(:has(.dropdown)) {
  position: static;
}

Real-World Example: Complex Form

<form>
  <div class="form-row">
    <label>Email</label>
    <input type="email" required />
    <span class="helper">We'll never spam you</span>
  </div>

  <div class="form-row">
    <label>Password</label>
    <input type="password" required minlength="8" />
    <span class="helper">Minimum 8 characters</span>
  </div>

  <button type="submit">Sign Up</button>
</form>
/* Form validation with :has() */
.form-row:has(:focus-within) {
  background: var(--highlight);
}

.form-row:has(input:invalid) label {
  color: var(--error);
}

.form-row:has(input:valid) label {
  color: var(--success);
}

/* Show helper only when input is invalid */
.form-row:has(input:invalid) .helper {
  display: block;
  color: var(--error);
}

.form-row:has(input:valid) .helper {
  display: none;
}

/* Submit button state */
form:has(:invalid) button[type="submit"] {
  opacity: 0.5;
  cursor: not-allowed;
  background: var(--muted);
}

form:not(:has(:invalid)) button[type="submit"] {
  opacity: 1;
  cursor: pointer;
  background: var(--primary);
}

Key Takeaways

  • Multi-level selection: Select ancestors based on deep descendants
  • Performance: Scope selectors, use direct child checks (>)
  • Progressive enhancement: Use @supports selector(:has(*))
  • Specificity: :has() contributes based on most specific argument
  • Form validation: Create rich states without JavaScript
  • Quantity queries: Style based on child count
  • Combinators: Use +, ~, > inside :has()

Advertisement

Related Insights

Explore related edge cases and patterns

CSS
Surface
CSS contain: Render Isolation for Performance
6 min
CSS
Surface
CSS :has() — The Parent Selector That Changes Everything
6 min

Advertisement