Mastering CSS Logical Pseudo-Classes: :is, :where, :not, and :has
The :is() pseudo-class function accepts a selector list and matches any element that can be selected by one of the selectors in that list. This reduces repetition when applying identical styles to different elements under various contexts.
/* Target navigation links in multiple regions without repetition */
:is(.site-header, .page-footer, .sidebar) a {
text-decoration-thickness: 2px;
text-underline-offset: 4px;
}
Unlike comma-separated selectors, :is() forgives invalid selectors in the list, continuing to match valid ones rather than invalidating the entire rule. Specificity calculates based on the most specific selector in the list.
The :where() pseudo-class functions identically to :is() in matching behavior but contributes zero specificity to the overall selector. This proves invaluable for reset stylesheets and library code where minimal specificity impact prevents unintentional overrides.
/* Apply zero-specificity base styles that are easily overridden */
:where(button, input[type="submit"], .cta-button) {
padding: 0.75rem 1.5rem;
border-radius: 0.375rem;
cursor: pointer;
}
Developers can nest :where() to create complex conditional scopes without bloating specificity weights, making it ideal for theming systems where contextual overrides must remain lightweight.
The :not() negation pseudo-class excludes elements matching its selector argument. CSS Selectors Level 4 expanded this to accept complex selecter lists, enabling sophisticated filtering previously requiring multiple rules.
/* Select list items that are neither active nor disabled */
.menu-item:not(.is-active, [aria-disabled="true"]) {
opacity: 0.8;
transition: opacity 0.2s ease;
}
Note that :not() itself adds specificity equal to its argument. Chaining multiple negations or combining with high-specificity selectors can create brittle cascade dependencies, so reserve it for targeted exceptions rather than broad architectural patterns.
The :has() relational pseudo-class—often termed the "parent selector"—targets elements containing descendants that match its arguement. This eliminates previous limitations requiring JavaScript DOM traversal for parent-element styling.
/* Style cards differently when they contain media elements */
.card:has(> img, > video) {
display: flex;
flex-direction: column;
gap: 1rem;
}
/* Highlight form groups with invalid inputs */
.form-group:has(input:invalid) {
border-left: 3px solid #dc2626;
padding-left: 1rem;
}
While :has() offers powerful document traversal capabilities, implementers should consider performance implications on large documents, as browsers evaluate these selectors against complete subtree structures. As of 2024, all modern browsers support these Level 4 selectors, though legacy environments require progressive enhancement strategies or polyfills for equivalent functionality.