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:
!importantdeclarations (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-indexvalues like9999 - 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



