:is :where :has
Selector engines that cut repetition — :is and :where for lists, specificity traps, and :has for parent/sibling state without JS.
- css
- selectors
- has
Three pseudo-classes reshaped how we write selectors: :is() and :where() group alternatives; :has() is the long-requested “parent selector” (more precisely, a relational selector). Together they replace whole families of duplicated rules — and introduce specificity and performance nuances you should know cold.
Docs: MDN :is(), :where(), :has().
:is() — grouping with real specificity
/* Instead of three rules */
:is(h1, h2, h3).title {
line-height: 1.2;
}
.card :is(a, button) {
font-weight: 600;
}
Specificity of :is() is the specificity of its most specific argument.
:is(.chip, #featured) {
/* counts as an ID when #featured is in the list */
outline: 2px solid red;
}
Forgiving parsing: invalid selectors inside :is() are dropped, not the whole list (unlike older selector lists in some contexts).
:where() — grouping with zero specificity
:where(h1, h2, h3, h4, h5, h6) {
margin-block: 0 0.5em;
font-weight: 650;
}
.prose h2 {
margin-block-start: 1.5em; /* easy override */
}
Design-system base styles love :where() so a single class can override without wars.
:has() — style based on descendants/siblings
/* Card with an image gets different padding */
.card:has(img) {
padding: 0;
}
.card:has(img) .card__body {
padding: 1rem;
}
/* Form field invalid state on the group */
.field:has(:invalid:not(:placeholder-shown)) {
border-color: var(--danger);
}
/* Previous-sibling-ish patterns */
label:has(+ input:focus) {
color: var(--brand);
}
/* Navigation current section highlighting parents */
.nav__item:has(.nav__link[aria-current="page"]) {
background: var(--brand-soft);
}
:has() is powerful enough to replace small amounts of class-toggling JS for pure presentational state derived from the DOM.
Combining them
.table-row:has(:is(input:checked, [aria-selected="true"])) {
background: var(--selected);
}
:where(.btn, .chip):has(svg) {
display: inline-flex;
gap: 0.35rem;
align-items: center;
}
Performance awareness
:has() can be costlier than simple class selectors because it may need to check relationships as the DOM mutates. Prefer:
- Shallow relationships (
.card:has(> img)when possible) - Avoid extremely broad
.page:has(…)on huge trees for frequent updates
In practice for typical UI it’s fine; measure if you attach it to rapidly updating lists.
Interview out-loud
“:is() groups selectors and takes the highest specificity of its arguments. :where() groups with zero specificity for low-priority defaults. :has() styles an element based on matching relatives — parent selectors for practical UI. I use :where for base, :is for DRY variants, and :has for structural state without extra classes.”
Footguns
- ID inside
:is()silently raising specificity. - Overusing
:hasinstead of a clear state class for complex app state. - Expecting
:hasin very old browsers without fallbacks. - Circular thinking — styles that constantly reflow under
:hasconditions. - Forgetting
:hasrelative selectors still need valid combinators.
Replacing old preprocessor lists
/* Old hand-expanded */
/* .btn:hover, .chip:hover, .link:hover { } */
:is(.btn, .chip, .link):hover {
color: var(--brand);
}
:where(.prose :is(h1, h2, h3, h4)) {
line-height: 1.25;
text-wrap: balance;
}
:has for form UX without JS classes
.password-field:has(input:focus) .strength {
opacity: 1;
}
.form:has(:invalid:not(:placeholder-shown)) .form-status {
color: var(--danger);
}
Keep server validation authoritative. CSS :has only styles; it does not secure anything. For app state that is not in the DOM (async “saving…”), a class from JS remains clearer than inventing hidden DOM flags.
Related
- CSS specificity deep dive
- CSS selectors attribute and sibling
- Cascade & specificity
- Pseudo-classes hover focus focus-visible
Further reading
Related guides
- CSS Selectors Attribute and SiblingAttribute selectors, combinators, and sibling selectors — match states and structure without extra classes when the DOM already has the signal.
- Accent Color and Form ControlsUse accent-color to theme native checkboxes, radios, ranges, and progress — what it paints, what it skips, and when to custom-style instead.
- Auto-fit vs Auto-fillauto-fit collapses empty tracks; auto-fill keeps them. Use both with minmax for responsive card grids without media-query soup.
- BEM Naming MethodologyBlock, Element, Modifier naming for CSS — how it limits specificity wars, what to name, and when utility or CSS modules beat strict BEM.
- blend-mode Basicsmix-blend-mode vs background-blend-mode — how pixels combine, isolation, and the stacking/readability traps in product UI.