Specificity of a CSS selector

Write codeMedium25 min

Your task

A style linter needs the specificity of a selector without a browser. Given one complex selector, return its specificity as [a, b, c]:

  • a counts ID selectors: #main
  • b counts class selectors (.item), attribute selectors ([type="checkbox"]) and pseudo-classes (:hover, :nth-child(2n+1))
  • c counts type selectors (li) and pseudo-elements (::before)

The universal selector * and the combinators (whitespace, >, +, ~) count for nothing. Four pseudo-elements also have a legacy one-colon spelling, and :before, :after, :first-line and :first-letter count as pseudo-elements in c, exactly like their two-colon forms.

Some functional pseudo-classes take a comma-separated selector list and do not count as an ordinary pseudo-class:

  • :is(...), :not(...) and :has(...) add the specificity of the most specific selector in their list: the maximum, not the sum. Specificities compare a first, then b, then c.
  • :where(...) always adds zero.

Selectors in those lists can contain further functional pseudo-classes to any depth, and a selector inside :has(...) may begin with a combinator, as in :has(> img). Every other pseudo-class counts as one in b, functional or not, and its argument is ignored (the of S form of :nth-child does not appear).

Pseudo-elements never take arguments. Names are lowercase letters, digits, hyphens and underscores. An attribute selector runs from [ to the next ] and its contents never contain ]. The input is a single valid selector (no comma outside parentheses), and whitespace may appear around combinators, commas and parentheses.

Examples:

  • selector_specificity("#nav .item > a:hover") returns [1, 2, 1]
  • selector_specificity("ul li:not(.done, #pinned)::before") returns [1, 0, 3]: :not adds #pinned, the more specific of its two selectors
  • selector_specificity(":where(#app) .btn") returns [0, 1, 0]

Examples

selector_specificity(selector: str) -> list[int]
Example 1
Inputselector_specificity("#nav .item > a:hover")
Output[1, 2, 1]
Example 2
Inputselector_specificity("ul li:not(.done, #pinned)::before")
Output[1, 0, 3]
Example 3
Inputselector_specificity(":where(#app) .btn")
Output[0, 1, 0]