A year of fast-moving feature work had left our main product's Tailwind config full of arbitrary one-off values, a spacing of 17px here, a color hex pasted in there. Nobody set out to build it this way. It accumulated one deadline at a time, each individual choice reasonable on its own and collectively a mess. We spent a sprint auditing the whole thing and pulling everything back into the design token system, and the process taught us more about how design debt actually accumulates than any postmortem could have.
What the audit found
Grepping the codebase for Tailwind's arbitrary value syntax, anything wrapped in square brackets like `w-[137px]` or `text-[#2b6f4a]`, surfaced dozens of one-off styles that should have been token references from the start. We expected to find a handful. We found well over a hundred across the app, concentrated heavily in three feature areas that had shipped under the tightest deadlines that year.
Most of these existed for a mundane reason: the token system was missing a value someone needed in the moment, and reaching for an arbitrary value was faster than filing a request to extend the scale. That told us as much about gaps in our tokens as about developer discipline. A spacing scale that jumps from 12 to 16 with nothing in between will accumulate arbitrary values wherever a design calls for 14, no matter how well-intentioned the team is about following the system.
We categorized every arbitrary value we found into two buckets: ones that mapped cleanly onto an existing token once we actually looked, and ones that represented a genuine gap in the scale. The split was roughly even, which surprised us. We had assumed most violations were laziness. Instead, close to half were legitimate cases where the design system simply hadn't kept pace with what the product needed.
- Roughly 60% of the arbitrary values we found could be mapped directly onto an existing token once we looked closely, meaning they were never actually necessary; they were just faster to reach for than searching the config for the right name.
- The remaining 40% represented genuine gaps, mostly in spacing and a handful of one-off brand colors used in marketing pages that had never been formalized into the shared palette.
- A small number, maybe a dozen, were pixel-perfect values pulled straight from a Figma inspector without anyone checking whether a nearby token would have looked identical after rounding.
Rebuilding the token layer
Rather than just deleting arbitrary values and hoping the closest existing token looked close enough, we treated the genuine gaps as real signal and extended the scale deliberately. That meant a few new spacing steps filled in where the jump between adjacent values was too large for common layout needs, and two brand colors promoted from ad hoc hex codes to named tokens with documented usage guidance describing exactly where each one was appropriate. That took longer than a pure find-and-replace would have, but it meant the resulting config reflected actual design intent rather than just fewer magic numbers scattered through the codebase.
We also split the config into logical sections, colors, spacing, typography, and shadows, each with a short comment block explaining when to extend it versus when to reach for composition instead. This sounds like a small thing, but it turned the Tailwind config from a file people avoided touching into one new hires could actually read and understand within their first week on the team. Previously, the config had grown into a nine-hundred-line file with no internal structure, and anyone extending it just added to the bottom, which is exactly how you end up with duplicate tokens that mean almost the same thing.
The process change that stuck
The cleanup itself mattered less than the process change that came out of it. Any arbitrary value in a pull request now needs a comment justifying it, enforced lightly through code review rather than a lint rule at first. We considered writing a custom ESLint rule to flag arbitrary Tailwind values automatically, and eventually did, but only after a few months of manual review had shown us which false positives to exclude: storybook snapshot classes, a codegen'd chart library wrapper that genuinely needs pixel-precise widths, and a couple of one-off marketing pages that didn't warrant new tokens for values used exactly once.
The lint rule now fails CI on any new arbitrary value outside an allowlisted set of files, which has kept the config close to the state we left it in after the audit. Config sprawl has a way of creeping back in under deadline pressure if there's no mechanical check behind the policy. A comment convention alone would have decayed within a couple of quarters, the same way the original config had drifted in the first place. We tracked new arbitrary-value pull requests for the two months after the rule shipped and found essentially none outside the allowlist, which was a much cleaner result than the comment-only period had produced.
What we'd do differently
If we were starting this again, we'd introduce the lint rule immediately rather than waiting several months to write it, since most of the value came from the mechanical enforcement rather than the one-time cleanup. We'd also involve design earlier in the process. Several of the token gaps we found were things designers had already noticed informally but never had a clean channel to report, and formalizing that feedback loop earlier would have caught some of the drift before it reached a hundred arbitrary values scattered across the codebase.
The audit itself took about a week of one engineer's time, spread across grepping, categorizing, and migrating values, plus another few days of design review on the new tokens we added to close the genuine gaps. Given how much smoother pull request reviews have gotten since, with reviewers no longer needing to eyeball every style value for whether it matches the design system, that week paid for itself within the first month. We now run the same grep-and-categorize exercise quarterly as a lightweight health check, mostly to catch the handful of exceptions that inevitably slip through the allowlist over time.
Measuring the actual impact
We didn't have a great way to quantify "cleaner config" before we started, so we defined a couple of rough proxies after the fact: the total line count of the config file, the number of distinct color values in use across the app, and the average time a reviewer spent per pull request on style-related comments, pulled from a sample of review threads before and after the audit. The config shrank by close to a third even after we added the new tokens to fill genuine gaps, since consolidating duplicate near-identical values more than offset the additions. Distinct color values in active use dropped from over forty to under twenty, most of which turned out to be barely distinguishable shades that had crept in independently across different features.
Reviewer time on style comments dropped noticeably too, though that number is fuzzier since it depends on which pull requests happened to touch styling in a given week. Anecdotally, several reviewers mentioned that reviewing styles had gone from a genuine chore, squinting at a diff to guess whether a new arbitrary value was reasonable, to a much faster check against a lint rule that either passed or didn't.
Extending the approach to other tools
Once the Tailwind audit had visibly paid off, a couple of other teams asked whether the same approach would work for other parts of the design system, our icon library and a set of shared animation timing values in particular. The general method transferred well: grep for one-off usage, categorize into "should be a token" versus "reveals a real gap," and follow up with a mechanical enforcement rule rather than relying on review discipline alone. The animation timing audit in particular found a similarly high rate of near-duplicate values, four different values all approximating "250 milliseconds," which consolidated into a single token without anyone noticing a visual difference.
Where the process still has gaps
The lint rule catches new arbitrary values in code, but it doesn't catch drift introduced through design handoff, a designer specifying a slightly off-token value in Figma that then gets implemented faithfully and technically passes review because it happens to already exist as an arbitrary value elsewhere. Closing that gap would mean linking our design tooling more tightly to the same token source of truth the codebase uses, which is a larger project we've scoped but not yet started. For now, we catch that category of drift the old-fashioned way, through periodic manual review, which is imperfect but has kept the problem from growing back to its original scale.