Compare tuples one column at a time
CSS specificity is a weight used as part of the cascade when competing declarations apply to the same element. The Selectors specification and MDN's specificity guide describe the familiar three-column notation: ID selectors, class-like selectors, and type-like selectors. Compare the leftmost column first. If it differs, lower columns do not overturn it; if it ties, continue to the next column. This is why one ID usually outweighs many class selectors within that comparison stage.
An ID selector contributes to the first column. Classes, attributes, and ordinary pseudo-classes contribute to the second. Type selectors and pseudo-elements contribute to the third. The universal selector and combinators contribute no weight. A selector list is not one combined score: each selector alternative has its own weight for the elements it matches.
The tuple is not a full model of the cascade. Origin, importance, cascade layers, scoping proximity, and order can affect which declaration wins before or after specificity is compared. If a style is surprising, inspect the complete cascade context in browser developer tools instead of adding another ID to force a result.
Read functional pseudo-classes precisely
The functional pseudo-classes :is(), :not(), and :has() take selector arguments. Their own pseudo-class name does not add the ordinary class-like weight; the argument contributes the highest specificity among its alternatives. For example, the specificity of :is(.card, #featured) includes the ID weight because #featured is the most specific argument, even for an element matched through .card.
The :where() pseudo-class has a different purpose: it and its arguments contribute zero specificity. It is useful for writing selectors that express context without making an override difficult. A long :where() argument can still affect which elements match even though it does not add to the tuple.
The :nth-child() and :nth-last-child() pseudo-classes add a class-like contribution. When the modern “of selector” form is used, the selector list after “of” also contributes its most specific alternative. Keep the pseudo-class contribution and the argument contribution separate when calculating the result.
Pseudo-elements such as ::before count in the type-like column. Pseudo-classes such as :hover count in the class-like column. This simple distinction covers many everyday cases, but functional forms and newer selector syntax are where a manual count is easiest to get wrong.
Use the selector bench as a reading aid
Benchlight's CSS Specificity Bench accepts a selector list, splits alternatives, and reports a tuple for each one. It handles common IDs, classes, attributes, types, pseudo-elements, the listed functional pseudo-classes, and :nth-child() arguments. In a browser it also asks the native selector parser to reject invalid syntax. It does not apply declarations to a page, match an element, or model the rest of the cascade.
For article#release .note:where(.muted), the tuple is (1, 1, 1): #release supplies the ID, .note the class, and article the type. :where(.muted) adds zero. Compare only declarations that compete on the same element, within the complete cascade context. The exercise is most useful when you can connect the tuple to a real browser rule.
Watch for parsing boundaries
Commas inside :is() or :not() are part of a function's argument, not top-level selector alternatives. Commas inside quoted attribute values are also not separators. A correct parser must track nesting and quoted strings before splitting a list. Escaped identifiers, namespaces, shadow-DOM-specific pseudo-classes, and newer syntax add further complexity.
The local bench is a focused calculator, not a complete CSS parser. If it reports a surprising value for an advanced selector, verify the selector with a standards-aware tool or the browser and consult the current Selectors specification. Do not treat a plausible tuple as proof that the selector is valid or supported in every target browser.
Fix the rule at the right layer
When two declarations compete, first check whether they are in the same origin and cascade layer and whether either is important. Then compare selector weights, scope, and source order. A value from a higher-priority layer can win despite a larger tuple elsewhere. An inline style, a user stylesheet, or an important declaration can change the comparison in ways that a selector-only tool cannot see.
Prefer making component boundaries and cascade layers clear rather than escalating specificity without limit. If a rule is difficult to override, reduce unnecessary IDs or use a deliberate low-specificity base. A local fix should explain which competing declaration it is meant to outrank. Test the result in the actual page and relevant states.
Calculate three selectors before choosing an override
Paste this selector list into CSS Specificity Bench:
article#release .note:where(.muted),
:is(.card, #featured) .title,
li:nth-child(2n of .item, #chosen)
The first selector has specificity (1, 1, 1): one ID, the note class, and the article type. The where pseudo-class and its muted argument contribute zero. The second has (1, 1, 0): the most specific is argument contributes one ID, and title contributes one class. The third has (1, 1, 1): li contributes a type, nth-child contributes a pseudo-class, and the strongest selector after of contributes an ID. The arithmetic is independent of whether a matching element currently exists.
These are three tuple calculations, not a ranking of declarations on this page. Only declarations that apply to the same element and property can compete. If both of the first two selectors matched the same element in the same origin, importance, layer, and scope context, the first tuple would win at the type column after the other columns tie. If a higher-priority cascade condition differs, comparing these tuples alone is insufficient.
To lower the first selector's weight while retaining its match conditions, :where(article#release) .note:where(.muted) has specificity (0, 1, 0). The ID still restricts matching inside where, but no longer raises specificity. This can be useful for a configurable component base. It is not automatically an appropriate replacement for a deliberate override.
Verify the selector in the browsers the project supports, particularly the of syntax and newer functional forms. The local bench asks the current browser's selector parser to validate the input, so a result in one browser does not establish support elsewhere. Finally inspect a matching element's computed style and the winning declaration. That connects the arithmetic to the actual conflict instead of escalating selector weights until a screenshot happens to look right.
