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.active␣a:hover
Try one
Customize
Options
Installation
npx shadcn@latest add https://craftbits.dev/r/specificity-calculator.jsonUsage
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-errorand setaria-invalidso 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
- Tuple math. Per W3C Selectors Level 4, specificity is a 3-tuple
[id, class, element]. IDs count toward columna, classes and attribute selectors count toward columnb, and type selectors and pseudo-elements count toward columnc. The universal selector and combinators contribute nothing. - 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). - 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. - Controlled vs uncontrolled.
selectoris the controlled value;defaultSelectorseeds the uncontrolled mode. Both modes fireonSelectorChangeso a parent can observe edits regardless of who owns the state. - Motion. Column values re-animate on change (spring
snap); bar fills use springsmooth. Both short-circuit underprefers-reduced-motion.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
selector | string | — | Controlled selector value. |
defaultSelector | string | "" | Initial selector for uncontrolled mode. |
onSelectorChange | (value: string) => void | — | Fired on every edit, both keyboard and example clicks. |
title | ReactNode | — | Optional heading above the input. |
description | ReactNode | — | Optional sub-headline under the title. |
placeholder | string | "type a selector..." | Empty-state placeholder. |
examples | ReadonlyArray<string> | 5 presets | Clickable example selectors. |
hideTokens | boolean | false | Hide the per-token pill breakdown. |
hideExamples | boolean | false | Hide the example selectors row. |
headingAs | 'h2' | 'h3' | 'h4' | 'h3' | Tag for the title element. |
className | string | — | Merged onto the root via cn(). |
Engine exports
| Export | Signature | Notes |
|---|---|---|
computeSpecificity | (selector: string) => SpecificityTuple | Total specificity for the selector. |
tokenizeSelector | (selector: string) => SpecificityToken[] | Per-token breakdown with kind and weight. |
compareSpecificity | (a, b) => number | Lexicographic comparison; positive = a wins. |
formatSpecificity | (tuple) => string | Renders a tuple as [a, b, c]. |
isValidSelector | (selector: string) => boolean | Structural check — balanced brackets, no trailing combinator. |
Accessibility
- The wrapper is a
<section>withdata-cb-edu="specificity-calculator". - The text input carries an explicit
<label>andaria-describedbypointing at the tuple summary. Invalid input setsaria-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-labellikespecificity 1, 2, 0so the value is conveyed without depending on the brackets. - The token list is a
role="list"ofrole="listitem"pills withdata-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 pairedSelector AvsSelector Bcomparison 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/uncontrolledselectorAPI, and ships the comparison helpers as named exports so consumers can build their own VS layout on top.