On this page
Grouping selectors can reduce repetition, but not every grouping mechanism has the same effect on the cascade. The :is() and :where() pseudo-classes can match the same elements while producing very different specificity. Understanding that difference helps you write reusable defaults without making every later change require a stronger selector or an important declaration.
Start with the override you want to allow
Before shortening a selector list, decide who should be able to override the rule. A base typography rule should usually yield to an ordinary component class. A narrowly targeted application state may need more weight. Override policy is a better starting point than choosing whichever syntax looks shortest.
If your team is moving declarations into utilities, use the CSS to Tailwind converter to explore declaration mappings after you identify the intended styles. The converter is not a specificity analyzer: retain the original selector context and verify which rule wins in the browser before replacing it with classes.
Read specificity as three columns: IDs, classes and related selectors, then element selectors and pseudo-elements. Compare the columns from left to right rather than adding them into a single decimal score. Ten class selectors do not become equivalent to one ID. The MDN specificity guide gives a fuller account of those rules.
Use :is() to group matching alternatives
The :is() pseudo-class matches an element that matches any selector in its argument list. It is especially useful when the same descendant pattern would otherwise be repeated for several parents. The pseudo-class itself adds no separate class-column weight; its contribution comes from its arguments.
/* These alternatives all use one class. */
:is(.article, .preview, .help-panel) h2 {
margin-block-start: 2rem;
}Each argument here has a specificity of 0-1-0. Adding h2 outside the function gives the complete selector a specificity of 0-1-1. That is similar to a straightforward .article h2 rule and is often exactly what you expect.
The important rule is maximum argument specificity. The most specific selector anywhere in the list determines the function's contribution, even when the element matched a less specific branch. An ID in one branch can therefore strengthen every match made by the grouped selector.
:is(.article, #featured) h2 {
color: rebeccapurple;
}This selector has specificity 1-0-1, including when it matches an h2 inside .article rather than #featured. It is not equivalent in cascade weight to writing .article h2 and #featured h2 as separate rules. The MDN :is() reference documents this behavior and the supported selector syntax.
Use :where() for intentionally lightweight defaults
The :where() pseudo-class matches alternatives in the same general way, but the function and all its arguments contribute zero specificity. This makes it particularly useful for reset rules, base component styling, and reusable library defaults that consumers should be able to override easily.
:where(.prose, #preview) h2 {
color: #24334a;
}
.warning-title {
color: #8c251d;
}The first selector has specificity 0-0-1 because h2 remains outside the function. The .warning-title class has specificity 0-1-0 and can win within the same cascade context. The ID inside :where() adds nothing, which is the precise difference from the earlier :is() example.
If you want the whole selector to contribute zero, put the entire selector inside the function. Placement is part of the design, not merely a formatting preference.
:where(.prose h2, .prose h3) {
line-height: 1.2;
}Low specificity does not mean a declaration is inactive or unimportant to the design. It means the default has a low barrier to intentional overrides. Check MDN's :where() documentation when defining a shared stylesheet contract.
Calculate the entire selector, not just the function
Selectors outside either function still count. A leading class, attribute selector, or state pseudo-class can provide most of the weight even when the grouped portion is zero. To debug a rule, break it into pieces and count the contribution of each piece explicitly.
.card :where(h2, h3) { /* 0-1-0 */
margin: 0;
}
:where(.card) :is(h2, h3) { /* 0-0-1 */
margin: 0;
}
.card:is(:hover, :focus-within) { /* 0-2-0 */
outline: 2px solid currentColor;
}In the final example, .card contributes one class-column unit and the strongest state argument contributes another. Neither function is a way to erase unrelated parts of a selector. Selector placement determines what is neutralized and what retains its ordinary weight.
Both functions accept forgiving selector lists: an invalid or unsupported argument can be discarded without necessarily invalidating every other argument. That helps progressive enhancement, but it does not guarantee the remaining branches provide an adequate experience. Keep essential baseline styles independent of an optional selector feature when your supported browsers require it.
Avoid dense combinations merely to compress a stylesheet. A long selector with several nested functions can be harder to audit than two clear rules. Prefer grouping that expresses a recognizable component relationship rather than grouping every declaration that happens to be identical today.
Debug the cascade before increasing specificity
Specificity is only one stage of the cascade. Origin, importance, and cascade layers are considered before specificity comparisons within the relevant context. Source order breaks ties later. A low-specificity unlayered normal rule can beat a higher-specificity normal rule in a layer, so counting selectors alone may not explain the result.
Open the browser's styles panel, identify the winning declaration, and check the file, layer, importance, and selector. Disable the winning rule temporarily to confirm your diagnosis. Do not add an ID or important declaration before understanding why the current declaration loses.
For a practical regression test, create a component with its ordinary state, a modifier class, a nested instance, and a theme wrapper. Confirm that a simple consumer class overrides the documented defaults. This override fixture makes future refactors safer, especially when someone replaces a repeated selector list with :is().
Keep selector review alongside layout review in your development workflow. The existing grid versus flexbox guide offers useful component examples for building fixtures, while the HTML and CSS collection provides related implementation guidance. Readable rules are easier to preserve than clever specificity tricks.
Key takeaways
- :is() takes the specificity of its strongest argument, not the matched branch.
- :where() and its arguments always contribute zero specificity.
- Count every selector outside the function normally.
- Check layers, importance, and origin before blaming specificity.
- Test that consumers can override shared defaults using the documented API.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor