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.
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: