Specificity Calculator

An interactive sandbox for CSS Selectors Level 4 specificity. Type a selector, the widget tokenises it, totals the [id, class, element] tuple, and shows which token contributed to which column. A row of preset examples doubles as a tour of the modern selector surface.

Preview

Specificity calculator

Type a selector — the tuple, the columns, and the per-token breakdown update live.

ID1
Class3
Element1
tuple[1, 3, 1]
#main.card.activea:hover
Try one
Customize
Options

Installation

npx shadcn@latest add https://craftbits.dev/r/specificity-calculator.json

Usage

import { SpecificityCalculator } from "@craft-bits/core";
 
<SpecificityCalculator
  title="Specificity calculator"
  defaultSelector="#main .card.active"
/>

Drive it as a controlled input — useful for lessons that quiz the reader against a hard-coded selector:

const [selector, setSelector] = useState("nav ul li a:hover");
 
<SpecificityCalculator
  selector={selector}
  onSelectorChange={setSelector}
  examples={["nav ul li a:hover", "[type=\"text\"]::placeholder"]}
/>

Headless usage of the engine — the helpers are exported so you can compare two selectors or grade a quiz answer without rendering the widget:

import {
  computeSpecificity,
  compareSpecificity,
  formatSpecificity,
} from "@craft-bits/core";
 
const a = computeSpecificity("#main .card");
const b = computeSpecificity("nav ul li a");
const verdict = compareSpecificity(a, b);
const label = formatSpecificity(a);

Anatomy

  • Input. A single text field labelled Selector. Invalid selectors paint the border in --cb-error and set aria-invalid so screen readers announce the state.
  • Columns. Three stacked cards — ID, Class, Element — each with a count, a bar fill, and a tone matched to the column's weight class.
  • Tuple line. A compact [a, b, c] summary that re-animates whenever the value changes.
  • Tokens. A flex row of pills, one per parsed segment, coloured by kind (id, class, attribute, pseudo-class, pseudo-element, element, combinator, universal). Whitespace combinators render as a visible glyph so the descendant chain reads cleanly.
  • Examples. A row of preset selectors below the input. Clicking one populates the field and fires onSelectorChange.

Understanding the component

  1. Tuple math. Per W3C Selectors Level 4, specificity is a 3-tuple [id, class, element]. IDs count toward column a, classes and attribute selectors count toward column b, and type selectors and pseudo-elements count toward column c. The universal selector and combinators contribute nothing.
  2. Comparison. Tuples are compared column-by-column, left to right — a higher column dominates everything to its right regardless of magnitude. [1, 0, 0] (one ID) beats [0, 10, 10] (ten classes plus ten elements).
  3. Selectors Level 4 pseudos. :is(), :not(), and :has() take the highest specificity of their arguments. :where() always contributes [0, 0, 0]. The engine recursively computes both.
  4. Controlled vs uncontrolled. selector is the controlled value; defaultSelector seeds the uncontrolled mode. Both modes fire onSelectorChange so a parent can observe edits regardless of who owns the state.
  5. Motion. Column values re-animate on change (spring snap); bar fills use spring smooth. Both short-circuit under prefers-reduced-motion.

Props

PropTypeDefaultDescription
selectorstringControlled selector value.
defaultSelectorstring""Initial selector for uncontrolled mode.
onSelectorChange(value: string) => voidFired on every edit, both keyboard and example clicks.
titleReactNodeOptional heading above the input.
descriptionReactNodeOptional sub-headline under the title.
placeholderstring"type a selector..."Empty-state placeholder.
examplesReadonlyArray<string>5 presetsClickable example selectors.
hideTokensbooleanfalseHide the per-token pill breakdown.
hideExamplesbooleanfalseHide the example selectors row.
headingAs'h2' | 'h3' | 'h4''h3'Tag for the title element.
classNamestringMerged onto the root via cn().

Engine exports

ExportSignatureNotes
computeSpecificity(selector: string) => SpecificityTupleTotal specificity for the selector.
tokenizeSelector(selector: string) => SpecificityToken[]Per-token breakdown with kind and weight.
compareSpecificity(a, b) => numberLexicographic comparison; positive = a wins.
formatSpecificity(tuple) => stringRenders a tuple as [a, b, c].
isValidSelector(selector: string) => booleanStructural check — balanced brackets, no trailing combinator.

Accessibility

  • The wrapper is a <section> with data-cb-edu="specificity-calculator".
  • The text input carries an explicit <label> and aria-describedby pointing at the tuple summary. Invalid input sets aria-invalid="true".
  • The column region is wrapped in aria-live="polite" so screen readers announce updates without interrupting the user.
  • The tuple line carries an aria-label like specificity 1, 2, 0 so the value is conveyed without depending on the brackets.
  • The token list is a role="list" of role="listitem" pills with data-cb-kind="..." so consumers can extend tone-specific styling without monkey-patching CSS.
  • Animations short-circuit under prefers-reduced-motion.

Credits

  • Extracted from: terminal-dreams (src/components/frontend-design/perf-css/ui/SpecificityCalculator.tsx). The original was a paired Selector A vs Selector B comparison driven entirely by props from a parent CSS-perf lab — tuples, tokens, validity, and the winner verdict all lived in the parent. This rewrite folds the tokenisation engine into the component, exposes a single controlled/uncontrolled selector API, and ships the comparison helpers as named exports so consumers can build their own VS layout on top.