Issue №131 Summer 2026A journal of dev tools, libraries & small ideasUpdated weekly
frontend · 4 min read
frontendJune 22, 2026 · 4 min

I built a tool that grades how fragile your CSS is

css-unused-finder is a hosted, browser-only tool that reads any stylesheet and grades how fragile it is — flagging `!important` pileups, ID selectors, runaway specificity, deep descendant chains, magic z-index values and duplicate selectors, then rolling them into a 0–100 fragility score and an A–F

css-unused-finder — grade how fragile your CSS is
01
frontend

CSS rarely breaks because a rule is wrong. The color is the color, the padding is the padding. It breaks because a rule is fragile — an !important that forces every future override to escalate, an ID selector nothing else can beat, a five-level descendant chain that quietly assumes the markup will never get reshuffled. The CSS works fine right up until someone moves a <div>, and then it doesn't.

I wanted a way to see that fragility before it bites, so I built css-unused-finder: paste a stylesheet, get a fragility grade and a list of dead classes back. It's a single page, it runs entirely in your browser, and there's no upload, account, or build step.

What it actually does

You paste CSS. It parses the stylesheet and runs a set of heuristics for the patterns that make CSS hard to change:

  • !important declarations (specificity nukes)
  • ID selectors (only another ID can override them)
  • overqualified selectors like ul.nav (welding a rule to one tag for no reason)
  • deep descendant chains (.a .b .c .d — brittle to markup changes)
  • the universal selector
  • runaway specificity (stacks of classes/attributes)
  • magic z-index values like 9999
  • duplicate selectors scattered across the file

Each finding has a severity, and the lot rolls up into a 0–100 fragility score and an A–F grade. If you also paste your HTML or JSX, it cross-references every class you've defined against every class you actually render and tells you which ones are dead.

The two decisions I'm glad I made

I hand-rolled the CSS parser. My first instinct was to reach for a real parser like PostCSS. But I didn't need a full AST — I needed selectors, declarations, and the brackets/strings/comments handled correctly so I don't trip over a } inside content: "}" or a { inside an attribute value. A focused, dependency-free parser turned out to be a couple hundred lines and kept the whole engine shippable as a tiny bundle that runs in the browser with zero runtime deps. The fiddly part isn't the happy path — it's the malformed input. Unterminated blocks, stray braces, @keyframes that I want to skip but still need to brace-balance past. Every one of those is a branch, and every branch is a chance to silently mis-parse, which is exactly why the next decision mattered.

I made specificity correct, not approximate. Specificity looks easy until you hit functional pseudo-classes. :where() contributes nothing regardless of its arguments. :not(), :is() and :has() contribute the specificity of their most specific argument — which means the calculator has to recurse into the parenthesized selector list and compare. A naive regex (count the dots, count the hashes) gets :is(#id, .class) flatly wrong. Getting it right meant walking the selector character by character, matching balanced parens, and recursing. It's more code, but for a tool whose entire job is to be trusted about CSS, "close enough" wasn't good enough.

To keep myself honest about all those edge cases, the engine ships at 100% test coverage — statements, branches, functions, lines. That number isn't vanity; for a parser it's the difference between "handles the examples I thought of" and "handles the unterminated comment, the trailing combinator, the empty :is(), the brace inside a string." Coverage forced me to write the test for every weird input before a user found it.

One design choice I want to call out because it's a judgment call: dead-class detection is deliberately conservative. When it sees className={clsx('foo', cond && 'bar')}, it can't evaluate the expression, so it treats every token it finds as "used." That means it can under-report dead classes but never over-report them. I'd much rather miss a dead class than confidently tell you to delete one that's wired up through a dynamic expression.

Try it

It's live here — paste your gnarliest stylesheet and hit Load example if you want to see it warm up first:

https://css-unused-finder-three.vercel.app

There's also a stateless JSON endpoint behind the same engine, which is handy in CI:

curl -X POST https://css-unused-finder-three.vercel.app/api/analyze \
  -H 'content-type: application/json' \
  -d '{"css":".a{color:red!important}","markup":"<div class=\"a\"></div>"}'

The whole thing — parser, specificity calculator, heuristics — is open if you want to read it: https://github.com/kea0811/css-unused-finder.

What's next

A few things on my list: per-finding "how to fix" snippets, an option to paste a URL and pull its stylesheets, and a shareable permalink for a report. But honestly the core loop — paste, read, fix — already does the thing I wanted, which is to turn a vague feeling of "this CSS is scary" into a specific, ranked list of what to change first. If you give it a spin and it flags something surprising in your stylesheet, I'd love to hear about it.


End of essay

About the author
Er An Khoo