Advanced Selectors: :is(), :where(), :not(), :has()

What You'll Learn

  • Use :is() to simplify complex selector groups and reduce repetition
  • Apply :where() for zero-specificity selectors that are easily overridden
  • Master complex :not() patterns with multiple arguments
  • Leverage :has() to select parent elements based on their descendants
  • Understand specificity differences between :is(), :where(), :not(), and :has()
  • Combine advanced selectors for sophisticated content-aware styling
  • Create adaptive layouts without JavaScript using :has()
  • Apply advanced selectors to real-world form validation and component patterns

Introduction to Modern Selectors

Modern CSS provides powerful pseudo-class functions that dramatically simplify selectors and unlock new capabilities:

  • :is() - Groups selectors to eliminate repetition
  • :where() - Like :is() but with zero specificity
  • :not() - Excludes elements matching selector lists
  • :has() - The long-awaited parent selector!

These selectors make CSS more expressive, maintainable, and powerful than ever before. They replace patterns that previously required JavaScript, and they're optimized by the browser for performance.

The :is() Selector - Grouping Made Easy

The :is() pseudo-class takes a selector list and matches any element that matches at least one of those selectors. This eliminates the need to write out every combination of selectors.

/* ❌ Old way - repetitive */ article h2, article h3, section h2, section h3, aside h2, aside h3 { color: #667eea; } /* ✅ With :is() - concise! */ :is(article, section, aside) :is(h2, h3) { color: #667eea; } /* Even simpler for single-level grouping */ :is(h1, h2, h3, h4, h5, h6) { font-weight: 600; line-height: 1.2; } /* Apply hover states to multiple buttons */ :is(.primary, .secondary):is(:hover, :focus) { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); }

The :where() Selector - Zero Specificity

The :where() selector works exactly like :is(), but it always has zero specificity. This makes it perfect for base styles that should be easily overridden.

/* :where() has 0 specificity - perfect for resets */ :where(ul, ol) { padding-left: 2rem; list-style-position: outside; } /* :where() for base styles, easily overridden */ :where(article, section) p { color: #666; /* Specificity: 0,0,1 (just the p) */ line-height: 1.8; } /* Simple class wins! */ .special-text { color: #28a745; /* Specificity: 0,1,0 - overrides! */ } /* Base button styles with :where() */ :where(button, .btn) { padding: 0.75rem 1.5rem; border: 2px solid #ddd; border-radius: 4px; } /* Easy to customize without !important */ .primary-btn { background: #667eea; color: white; }

The :not() Selector - Exclusion Patterns

Modern :not() accepts multiple selectors separated by commas, eliminating the need to chain multiple :not() pseudo-classes.

/* Old syntax - chaining */ button:not(.disabled):not(.secondary) { background: #667eea; } /* Modern syntax - list */ button:not(.disabled, .secondary) { background: #667eea; } /* Style all except first and last */ li:not(:first-child, :last-child) { border-top: 1px solid #ddd; border-bottom: 1px solid #ddd; } /* Only interactive inputs */ input:not([readonly], [disabled]) { border-color: #667eea; background: #f8f9ff; } /* Complex patterns */ .card:not(.featured, .archived) { border-color: #667eea; } .card:not(.featured, .archived):hover { transform: translateY(-4px); }

The :has() Selector - Parent Selection

The :has() pseudo-class is a game-changer: it selects elements based on their descendants. This is the "parent selector" developers have wanted for decades!

/* Style cards differently if they contain an image */ .card:has(img) { display: grid; grid-template-columns: 200px 1fr; gap: 1.5rem; } /* Style cards that contain a badge */ .card:has(.badge) { border-color: #667eea; border-width: 2px; } /* Form validation - style parent based on input state */ .form-group:has(input:invalid) { background: #fff5f5; border-color: #dc3545; } .form-group:has(input:invalid) label { color: #dc3545; } .form-group:has(input:valid) { background: #f0fff4; border-color: #28a745; } /* Style element if its next sibling matches */ li:has(+ li.featured) { border-bottom: 3px solid #667eea; } /* Empty state detection */ .container:not(:has(*)) { min-height: 200px; display: grid; place-content: center; }

Combining Advanced Selectors

The real magic happens when you combine these selectors to create sophisticated, expressive patterns that would be impossible (or very difficult) otherwise.

/* Sections containing any heading level */ section:has(:is(h2, h3, h4)) { background: #f8f9fa; padding: 1.5rem; } /* Base styles with easy overrides */ :where(button, .btn):not(.custom) { padding: 0.75rem 1.5rem; border: 2px solid #ddd; } /* Products with images that aren't sold out */ .product:has(img):not(:has(.sold-out)) { border-color: #28a745; cursor: pointer; } /* Table rows with errors but not archived */ tr:has(td.error):not(:has(td.archived)) { background: #fff5f5; } /* Articles that are blog posts or news, have images, not drafts */ article:is(.blog-post, .news-item):has(img):not(:has(.draft)) { border-left: 4px solid #667eea; } /* Interactive elements except disabled */ :is(button, a, input, select):not(:disabled, [disabled]) { cursor: pointer; } /* Sections without headings (accessibility concern) */ section:not(:has(:is(h1, h2, h3, h4, h5, h6))) { border: 2px solid orange; /* Highlight for review */ }

Specificity Comparison

Understanding how specificity works with advanced selectors is crucial for maintainable CSS:

Selector Specificity Notes
:is(.class, #id) 0,1,0,0 Takes highest specificity (ID)
:is(div, .class) 0,0,1,0 Takes highest specificity (class)
:where(.class, #id) 0,0,0,0 Always zero specificity
:not(.class) 0,0,1,0 Takes specificity of argument
:has(.class) 0,0,1,0 Takes specificity of argument
.class 0,0,1,0 Standard class selector
div 0,0,0,1 Standard type selector

Key takeaways:

  • :is(), :not(), :has() take the specificity of their most specific argument
  • :where() always has zero specificity, regardless of arguments
  • Use :where() for base styles, :is() for normal component styles
  • The pseudo-classes themselves don't add to specificity

Real-World Patterns

Here are practical patterns you can use in production code:

/* Consistent heading styles */ :is(h1, h2, h3, h4, h5, h6) { font-weight: 600; line-height: 1.2; color: #333; } /* Navigation and footer links */ :is(nav, footer) a { color: white; text-decoration: none; } /* Required invalid inputs */ input:is(:required, [aria-required="true"]):invalid { border-color: #dc3545; } /* Cards with media content */ .card:has(:is(img, video, iframe)) { aspect-ratio: 16/9; overflow: hidden; } /* Interactive elements except disabled */ :is(button, a, input, select):not(:disabled, [disabled], [aria-disabled="true"]) { cursor: pointer; } /* Table rows with errors */ tr:has(td.error) { background: #fff5f5; } /* Form groups with focused inputs */ .form-group:has(input:focus) { outline: 2px solid #667eea; outline-offset: 2px; } /* Articles with specific features */ article:has(img):has(blockquote) { /* Rich content layout */ max-width: 800px; margin-inline: auto; } /* Empty containers */ .container:not(:has(*)) { min-height: 200px; display: grid; place-content: center; } .container:not(:has(*))::before { content: "No content yet"; color: #999; } /* Checkbox parent styling */ .toggle:has(input:checked) { background: #28a745; color: white; } /* List item before featured */ li:has(+ li.featured) { border-bottom: 3px solid #667eea; padding-bottom: 0.5rem; }

Key Takeaways

  • :is() simplifies complex selector lists and takes the highest specificity from its arguments
  • :where() works like :is() but always has zero specificity, perfect for base styles
  • :not() now accepts multiple selectors, making exclusion patterns cleaner
  • :has() enables parent selection based on descendants, unlocking content-aware styling
  • Combining these selectors creates powerful, expressive patterns that replace JavaScript
  • Use :where() for resets/base styles, :is() for components, :has() for adaptive layouts
  • Modern selectors are browser-optimized and often faster than JavaScript alternatives
  • Understanding specificity is crucial: :where() = 0, others take argument specificity
  • Browser support is excellent for :is()/:where()/:not(), good for :has() in modern browsers