DEV Community

Joe Lin for BeGoodTool.com

Posted on

CSS Specificity Real Render Winner Rule Checker

I used to reach for a specificity calculator whenever a CSS declaration “did nothing.” It would return a neat (a,b,c) score, but I still had to remember source order, !important, whether the selector actually matched, and what value the browser computed after the cascade. A score is useful for explaining a conflict; it is not the same thing as observing the rendered result.

This tool takes a deliberately hybrid approach. It renders the pasted HTML and CSS in a hidden iframe, reads the browser's computed value, and separately parses the declarations to explain which matching rule should win. That gives you a concrete value and an inspectable reason instead of one opaque number.

The Browser Gets an Isolated Document

The analyzer creates a fixed, invisible iframe and writes only the supplied markup into it:

const iframe = document.createElement("iframe");
iframe.style.cssText =
  "position:fixed;top:-9999px;left:-9999px;width:1000px;height:600px;" +
  "opacity:0;pointer-events:none;z-index:-1";
document.body.appendChild(iframe);

const doc = iframe.contentDocument || iframe.contentWindow.document;
doc.open();
doc.write(
  `<!DOCTYPE html><html><head><style>${cssInput.value}</style>` +
  `</head><body>${htmlInput.value}</body></html>`
);
doc.close();
Enter fullscreen mode Exit fullscreen mode

The requested selector is resolved with querySelector, and the selected element is checked with getComputedStyle:

const computedStyle = iframe.contentWindow.getComputedStyle(el);
const computedValue = computedStyle.getPropertyValue(prop).trim();
Enter fullscreen mode Exit fullscreen mode

That matters for values such as colors, inherited properties, and browser normalization. The returned value is what this isolated document computed, not merely what a parser thinks the declaration says.

Specificity Is Still Useful as an Explanation

The component calculates the familiar three-part score. IDs count as a, classes, attributes, and pseudo-classes as b, and type selectors and pseudo-elements as c:

const score = idCount * 10000 + classCount * 100 + typeCount;
return {
  a: idCount,
  b: classCount,
  c: typeCount,
  score,
  str: `(${idCount},${classCount},${typeCount})`,
};
Enter fullscreen mode Exit fullscreen mode

Before counting, it expands the contents of :not(...) for counting and replaces attribute contents with [x]. It then removes IDs, classes, attributes, and pseudo-classes before looking for type selectors. This is a practical parser for the snippets the tool is designed to inspect, not a standards-complete CSS grammar.

Declarations are found with a regular expression, split on commas for multi-selector rules, and stored with their source order:

const declRe = /([\w-]+)\s*:\s*([^;!]+)(\s*!\s*important)?;?/g;
// ...
props[propName] = { value, important };
Enter fullscreen mode Exit fullscreen mode

For each requested property, matching rules are sorted by specificity score and then source order. An important declaration receives a million-point offset:

score: r.props[prop].important
  ? spec.score + 1000000
  : spec.score,
sourceOrder: r.sourceOrder,
Enter fullscreen mode Exit fullscreen mode

The results table keeps every matching declaration, highlights the first one as the winner, and shows !important beside it. If a property name contains color or background, the component also tests whether the computed value can be assigned to a temporary element so it can show a color swatch.

Why Computed Value and Winner Explanation Can Diverge

The iframe is the source of truth for the computed property value, but the displayed winner is derived from the component's simple parser and score. That distinction is worth keeping in mind when debugging shorthand properties, inheritance, browser defaults, or syntax the parser does not recognize. The tool also auto-detects property names by collecting up to twelve names that match ([\w-]+)\s*: in the CSS text; it does not inspect the browser's full computed-style list.

The good news is that this makes the output readable. You can see the actual value, the selector that the local explanation ranks first, all matching rules, and their (a,b,c) values in one table. You can also replace the defaults with a tiny reproduction instead of loading an entire application.

The default reproduction is intentionally small: a .text paragraph sits inside .container#main, and .text, .container .text, p.highlight, and #main p all compete over color and font-size. It is a useful sanity check because the winning ID rule is not the last rule in the stylesheet. You can also press “Auto-detect from CSS”; that helper collects property names with a simple regular expression, lowercases them, removes duplicates with a Set, and limits the list to twelve properties before filling the input.

Static Snippets Only

The iframe does not load external fonts or images and does not execute external JavaScript. Shadow DOM and styles dynamically injected by CSS-in-JS are outside this input model. The CSS parser is intentionally regex-based, so nested braces, unusual declaration values containing semicolons, or advanced syntax can be parsed differently from a production stylesheet. If the selector is invalid or no element matches, the component reports that rather than pretending there is a winner.

I turned this debugging workflow into a small free tool: CSS Specificity Real Render Winner Rule Checker. Paste the smallest HTML/CSS example that reproduces the conflict, then use the table to decide whether the fix belongs in the selector, source order, or the declaration itself.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.