12k
All articles

Auditing a Stylesheet With Project Wallace

Project Wallace CSS audit turns unique colors, font sizes, specificity, duplicates, and filesize into fixes for cleaner stylesheets.

OpenReplay Team
OpenReplay Team
Auditing a Stylesheet With Project Wallace

A CSS audit with Project Wallace means pasting a stylesheet into the online analyzer and reading five numbers: unique colors, unique font-sizes, maximum selector specificity (with the id and !important counts beside it), the gap between total and unique declarations, and uncompressed filesize against gzip filesize. Each number points at a different change.

If your stylesheet is three years old, you probably already suspect it has drifted. What you lack is a number to put in a ticket. “The CSS feels messy” does not get prioritised; “we ship six greys where the design defines two” does.

This article runs one small fixture stylesheet through the analyzer, reads the output metric by metric, and pairs each finding with the concrete change it should trigger. It covers diagnosis. The follow-on piece, How to Organize CSS in Modern Web Projects, covers the treatment.

Key Takeaways

  • Browsers discard CSS they cannot parse or do not recognise and keep rendering, so a stylesheet accumulates mistakes without ever failing a build.
  • When you paste or upload CSS, Project Wallace runs the analysis in a WebWorker on your own device, so the stylesheet never leaves the browser.
  • The gap between unique colors shipped and the palette the design defines is the measurable form of design drift, and the fix is consolidating values into tokens, not deleting rules.
  • A specificity spike mid-stylesheet costs more than a high maximum at the end, because every later override has to climb to match it.
  • Empty rules are the one audit fix that never needs a visual regression check; duplicate declarations need judgement before removal.

Why Does a CSS Audit Find What the Build Cannot?

A browser that meets a CSS declaration it cannot parse or does not recognise discards that declaration and keeps rendering the page, which is why a stylesheet can accumulate mistakes for years without a single build ever failing. That recovery is specified behaviour. Under the error handling rules in CSS Syntax Module Level 3, the half-built declaration is dropped, the parser moves on past the next semicolon, and normal parsing carries on from there.

The consequence is that CSS damage never looks like a failure. It looks like drift: a fourth grey that is two points off the third, a 17px heading between the 16px and 18px steps, an id selector added under deadline pressure and then an !important added to beat it. None of these break anything. All of them make the next change harder.

How Do You Run the Project Wallace Analyzer?

The Project Wallace CSS analyzer takes input three ways: a website URL, an uploaded file, or CSS pasted directly. When you paste or upload CSS, the work happens locally: a WebWorker on your own device does the analysis, and nothing you paste is sent anywhere. That design dates from the 2021 analyzer rewrite. URL mode necessarily fetches the target site over the network before analysis.

The input page has a “Prettify CSS?” toggle, with a note beside it warning that the option shifts the numbers a little. Pick a state and keep it fixed for every run you intend to compare.

Here is the fixture. Three contributors, two years, one header and one card component:

/* header.css — three contributors, two years */
#site-header {
  background: #f5f5f5;
  color: #333333;
  font-size: 16px;
}

#site-header .nav-link {
  color: #343434;
  font-size: 15px;
  padding: 8px 12px;
}

.nav-link:hover {
  color: #222222 !important;
}

.card {
  background: #f4f4f4;
  color: #333333;
  font-size: 1rem;
  padding: 16px;
}

.card .card-title {
  font-size: 18px;
  color: #333333;
}

.card--featured .card-title {
  font-size: 17px;
  font-weight: 700 !important;
}

.legacy-banner {
}

.footer {
  background: #f5f5f5;
  color: #444444;
  font-size: 14px;
}

What comes back is a results page grouped into the same categories as the metrics documentation: Stylesheet, Atrules, Rules, Selectors, Declarations, Properties, and Values. There are well over a hundred metrics. The five below are the ones that turn into a commit.

What Do Unique Colors and Font-Sizes Tell You?

The difference between total colors and unique colors tells you how often each color is reused, and the difference between unique colors and the palette your design system defines tells you how far the code has drifted from the design. The analyzer counts both and gives you the full list of unique colors it found; font-sizes get the same treatment.

Read by hand, the fixture contains six distinct hex strings: #f5f5f5 and #f4f4f4 for surfaces, and #333333, #343434, #222222, #444444 for text. A design system for this component almost certainly intended one surface and two text colors. The type scale is worse: 16px, 15px, 1rem, 18px, 17px, 14px are six values as written, and 16px and 1rem usually resolve to the same pixel size anyway.

The fix is consolidation, not deletion. Consolidating near-identical greys does not mean removing rules; it means replacing each literal with the nearest token and letting the rule keep doing its job:

:root {
  --color-surface: #f5f5f5;
  --color-text: #333333;
  --color-text-muted: #444444;
  --font-size-sm: 0.875rem;
  --font-size-base: 1rem;
  --font-size-lg: 1.125rem;
}

.card {
  background: var(--color-surface);
  color: var(--color-text);
  font-size: var(--font-size-base);
  padding: 16px;
}

Three color tokens replace six literals; three size tokens replace six. Project Wallace’s separate Design Tokens tool extracts candidate colors and font-sizes from existing CSS, which is a faster starting point than reading the list by hand on a real stylesheet.

Specificity: Spikes Matter More Than the Maximum

A high-specificity selector near the end of a stylesheet is a local problem, but one in the middle forces every later rule that needs to override it to climb to the same level, and that climb is how id selectors and !important flags multiply. Harry Roberts’ specificity graph makes the same argument visually: the trend should rise gently toward the end, and any spike is a cost paid by everything after it.

The analyzer reports specificity as a three-part id, class, type value through Maximum selector specificity, Total selectors having maximum specificity, and Top specificity selectors, alongside Total id selectors, Total !important declarations, and Ratio of !important declarations.

The fixture shows the mechanism in miniature. #site-header .nav-link sits at (1,1,0). The later .nav-link:hover at (0,2,0) cannot beat it, so a contributor reached for !important. One id selector produced one !important flag, and the second flag on .card--featured .card-title beats a rule that never set font-weight at all.

The id selector count and the !important count are the two specificity numbers a team can reduce one commit at a time and re-measure after each change. In the fixture, changing the markup from id="site-header" to class="site-header" flattens both header rules to (0,1,0) and (0,2,0), and both !important flags become unnecessary.

Duplicate Declarations and Empty Rules

An empty rule contributes bytes and a selector match but changes nothing the user sees, so removing it is the one audit fix that never needs a visual regression check. Duplicate declarations are different: the same intent written twice is a smell, but deleting the wrong copy changes the cascade.

The analyzer counts Total empty rules directly; .legacy-banner {} is the fixture’s one instance, and it goes. There is no standalone duplicate-declarations metric. Read Total declarations against Total unique declarations and treat the gap as the duplicate count, bearing in mind that the docs still leave open whether whitespace or formatting makes two declarations distinct.

color: #333333 appears in three rules of the fixture. The right move is not to delete two copies but to route all three through var(--color-text), which makes the repetition visible as a shared decision instead of a coincidence.

Why Does Gzip Filesize Hide a Bloated Stylesheet?

Gzip compresses repetition efficiently, so a stylesheet full of duplicate declarations can post a flattering compressed size while its uncompressed size, the bytes the browser actually parses, keeps growing. The analyzer reports Uncompressed filesize, Gzip filesize, and Gzip filesize compression ratio together.

A rising compression ratio is the tell: it means the stylesheet is getting more repetitive, not smaller. What moves uncompressed size is the number of rules and declarations, which is why the consolidation work above reduces it as a side effect. Filesize is a diagnostic to read after the other fixes, not a target to optimise on its own.

Tracking a Stylesheet Across Releases

To track a stylesheet across releases, save the raw CSS of each release next to its analyzer results and re-run every analysis with the same Prettify state. The CSS Diff viewer formats two pasted stylesheets and compares them line by line in the browser, which shows where the unique-color or id-selector count moved.

Conclusion

A stylesheet audit earns its time when each number maps to a change: unique colors and font-sizes to tokens, id selectors and !important flags to flatter specificity, empty rules to deletion, duplicates to shared declarations, and filesize to a check that the rest worked. Paste your largest production stylesheet into the analyzer, write down those five numbers, and open one pull request per number.

FAQs

Can I run the Project Wallace analyzer from the command line or in CI?

Yes. The wallace-cli npm package runs the same analyzer in a terminal: install it with npm install wallace-cli, then run wallace path/to/styles.css or pipe CSS in through stdin, and add the --json flag for machine-readable output in CI scripts. Version 4.x of the CLI requires Node 20.12 or later. For programmatic use, import the analyze function from @projectwallace/css-analyzer, an ESM-only package that runs in both Node and browsers.

Should I analyze my Sass or Tailwind source files or the compiled CSS?

Analyze the compiled CSS your users receive, not the Sass, Less or Tailwind source. Variables, mixins and @extend expand at build time, so source files misreport unique colors, selector counts and filesize. Project Wallace's own Stylelint plugin says the same thing about where to point it: aim it at the bundle you ship, because counts of unique values and the ratios built on them describe the delivered file rather than the source. URL mode already fetches the built stylesheets a site serves.

What is the difference between the Project Wallace CSS Analyzer and its CSS Code Quality tool?

The CSS Analyzer reports raw metrics; the CSS Code Quality tool takes that output, runs its own checks over it, and boils the result down to three scores out of 100, for Performance, Maintainability and Complexity. Use the analyzer when you need to trace a number to a specific rule, and Code Quality when you want an opinionated summary to share with a team. Both accept a URL, uploaded files or pasted CSS.

How do I stop unique colors or specificity from regressing after an audit?

Add @projectwallace/stylelint-plugin to your Stylelint config. It ships 60 plus rules built on the same analysis engine, including projectwallace/max-unique-colors, and its holistic preset judges the file as a whole (totals, averages, ratios, uniqueness) instead of checking one node at a time. A recommended preset gets you going with a single extends line. Run it against the built CSS bundle in CI so a pull request that adds a seventh grey fails before merge.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.