skip to content

You inherit a 12,000-line stylesheet and want to delete the rules nothing uses. How do you identify dead CSS, and why do coverage reports and automated purge tools over-report rules as unused?

level: seniorimportance: should knowfreq 42%

answer

  1. nothing errors on a rule that never matches
  2. coverage measures your session, not the app
  3. string scanning can't see built class names
  4. evidence, not proof
  5. delete files, not scattered rules

basics

~20 s

Nothing in CSS errors on a selector that matches nothing, so deletion is evidence-gathering, not compilation. Coverage tools report only what a recorded session exercised, and purge tools match class-name strings in source, so states, breakpoints and dynamically built class names all look dead when they are not.

solid answer

~50 s

Start by accepting that no tool can prove a rule is dead — CSS has no compile-time link between a selector and the markup it matches. Chrome DevTools' Coverage panel reports unused bytes for exactly the interactions you recorded, so anything behind `:hover`, `:focus`, an error state, a different breakpoint, or `@media print` shows as unused. Purge tools like PurgeCSS work differently: they scan your templates and scripts for strings that look like class names, so a class assembled at runtime as `'btn--' + variant` is invisible to them and gets stripped unless it is safelisted. So I treat both as *candidate generators*, then verify each candidate by grepping the class token across every source that can emit markup — templates, components, tests, CMS content, transactional emails. Then I delete in small per-component commits behind visual-regression screenshots, so a mistake is one cheap revert rather than an archaeology project.

code

bash · 1 line
bash
grep -rn "card--featured" ./src ./templates ./emails ./tests

go deeper

for a junior

Know that CSS never errors on a rule that matches nothing, which is why unused styles accumulate silently and why finding them takes deliberate searching rather than a compiler.

for a middle

Explain how the two tool families work and where each is blind: coverage measures one recorded session, purge tools string-match class names in source. Name the categories they miss — states, unmatched breakpoints, print, runtime-built class names.

for a senior

Show a procedure with a safety net: candidates from tooling, verification by searching every source that emits markup, small per-component commits, visual regression behind it — and be explicit that zero search hits is evidence, not proof.

for a principal

Argue the structural fix over the cleanup campaign: colocation so CSS dies with its component, literal class names so analysis works, and a tracked stylesheet-size trend so the problem is visible before it needs archaeology again.

## Why CSS has no compiler to tell you In most languages, deleting an unused function is safe because the build fails if you were wrong. CSS offers no such feedback. A selector that matches no element is not an error, not a warning, and not observable at runtime — it simply never applies. The link between a rule and the markup it styles is a string comparison performed by the browser against a DOM that may not exist yet, may be assembled by JavaScript, or may come from a CMS field a developer has never seen. That is the whole difficulty. Everything below is about accumulating enough evidence to make deletion an acceptable risk, because certainty is not available. ## What coverage tooling actually measures Chrome DevTools has a **Coverage** panel: start recording, use the page, stop, and it reports how many bytes of each CSS file went unused. It is genuinely useful and routinely misread, because it answers a narrow question — *which rules did not apply during this recording* — and people hear *which rules are dead*. Things that count as unused in a normal recording but are very much alive: - `:hover`, `:focus-visible`, `:active` rules for anything you did not interact with - state classes applied only on error, loading, or empty results - rules inside a `@media` block whose condition did not match your viewport - `@media print` rules - everything on the pages and routes you did not visit Coverage is a *starting list*, best used across a scripted walkthrough of many routes and viewports, and even then only to nominate candidates. ## Why purge tools over-report PurgeCSS, UnCSS and their equivalents take a different approach: they read your content files (templates, JSX, markdown) and extract every string token that could plausibly be a class name, then delete rules whose selectors reference tokens they never saw. This scales far better than a browser recording — but it is a text search, and text search cannot see a class name that does not exist as a literal: ```javascript // invisible to a content scanner: no literal "btn--danger" exists anywhere el.className = `btn--${variant}`; ``` The same blind spot covers class names supplied by a CMS or rich-text field, injected by a third-party widget, produced by server-rendered templates in another repository, or built from a lookup table. Every purge tool therefore ships a **safelist** escape hatch — and a safelist is an admission that the tool cannot decide. Teams that use one seriously write class names as complete literals (a lookup object mapping `variant` to the full class string) precisely so the scanner can see them. ## A safe deletion procedure 1. **Generate candidates** from coverage across a scripted route walkthrough, or from a purge tool run in report-only mode. Do not let either delete anything directly. 2. **Verify each candidate by searching the whole codebase** for the class token — not just the app's templates, but tests, email templates, documentation, seeded CMS content, and any sibling repository that renders the same markup. A single hit is enough to keep the rule; zero hits is *evidence*, not proof. 3. **Prefer deleting files over deleting rules.** If a component is gone, its whole stylesheet goes, and that decision is reviewable in a way that "removed 340 scattered rules" never is. 4. **Delete in small, per-area commits.** The value is revert granularity: when something breaks three weeks later, you want to undo one commit, not bisect a 4,000-line removal. 5. **Put visual regression behind it.** Screenshot diffs across the main routes and both themes catch the class of mistake that review does not — a rule nobody remembered was load-bearing. 6. **Watch the sheet size trend**, not a single audit. If the number goes down once and then resumes climbing, you fixed a symptom. ## Preventing the next 12,000 lines The reason the file got this big is structural: rules outlived what they styled because their file did not. The durable fix is colocation — a component's CSS in the component's file, so removing the feature removes the CSS as a side effect and nobody has to prove anything. Pair that with keeping literal class names literal, so whatever purge tool you adopt can actually see them, and the archaeology becomes a one-time cost rather than a recurring one.

  • A purge tool's safelist keeps growing. What is that telling you about the codebase?
    That class names are being assembled at runtime rather than written as literals, so the scanner cannot see them. Each safelist entry is a rule the tool has been told to stop reasoning about, which quietly shrinks the tool's usefulness. The fix is upstream: map variants to complete class strings in the source so the analysis works without exceptions.
  • Why is deleting a whole component stylesheet safer than deleting the same number of rules scattered through a global file?
    Because the decision has a single, checkable premise — is this component still used anywhere? — which you can answer by searching for its name. Scattered rules each need their own investigation, the review shows a wall of unrelated removals, and a revert three weeks later restores things you deliberately removed along with the one you got wrong.
  • What would make you decide not to delete dead CSS at all, even after identifying it?
    When the risk-to-reward is bad: a small sheet where the dead bytes are negligible, a component about to be rewritten anyway, or areas with no visual-regression coverage where a mistake surfaces only in production. Dead CSS costs comprehension and a few kilobytes; a broken checkout page costs more. Delete opportunistically where you already have safety, not as a campaign.

saying these in an interview costs you the question

  • Treats the DevTools Coverage report as proof a rule is dead
  • Lets a purge tool delete rules automatically with no verification
  • Forgets hover, focus, error and print rules
  • Assumes a class not found in templates is definitely unused
  • Deletes thousands of lines in one commit

context