An internal ops platform I work on had never had a design system, and it showed in specific, countable ways: thirteen primary-button call sites across seven distinct class strings, eleven tables with six different header conventions, a type scale with sixteen arbitrary pixel values, and — the one that actually mattered — roughly half the form controls in the app with no visible keyboard focus indicator at all. Over about twenty-four PRs, the fix was deliberately boring: lay down tokens, build a handful of primitives on top of them, migrate every screen one at a time, and write a lint rule that makes the drift mechanically impossible to reintroduce.
Six PRs of unification are worth nothing if the next screen hand-rolls an eighth primary button. The guard is not the cleanup; the guard is the point.
Tokens first, components second
The first PR restyled nothing. It laid down surface, text, line, brand, and status color tokens as CSS custom properties, each one annotated in the source with the exact contrast ratio that justified it — and fixed every accessibility failure that was measurable without touching layout. The sharpest of those: globals.css ended in a single rule, button:focus { outline: none; }, and only about half the form controls had ever put a ring back. Every button and checkbox in the tool had no visible keyboard focus indicator at all, a WCAG 2.4.7 failure sitting in the global stylesheet the entire time. It was replaced with one :focus-visible rule in the base layer, so components that already carried their own ring kept it and everything else inherited this one for free.
Forty variants, four primitives
With tokens in place, Button, Badge, EmptyState, and Field landed together and immediately retired the worst of the duplication: thirteen primary-button sites, five icon-button variants, the app's only purple button, and disabled-state opacity that had drifted across four different values. Button applies tone — default, success, critical — orthogonally to hierarchy rather than as an extra rank, because a destructive action can just as easily be a dialog's main action as a quiet row action, and a rank can't express that. Field replaced a shared class string that four different copies had each drifted slightly from, and switched its border from a decorative gray that measured 1.47:1 to one that clears the 3:1 minimum WCAG 1.4.11 sets for a border that's how you identify a control — fields are visibly darker now, and that's the fix, not a side effect.
The component tests caught the author's own mistakes twice in this one PR: Button's critical tone hardcoded a raw red hover shade — exactly the raw-palette-for-meaning the token layer exists to prevent — and a test asserting no raw palette class in the component failed on its own first draft. The same slip had already shipped one PR earlier, on the tokens PR's own success-tone buttons, and got caught the same way.
A bug the screenshot caught and the source code didn't
The next PR unified four different tile treatments — different radii, different label sizes, two different label-to-value orderings — into one StatTile, and in doing so hit a bug that no amount of reading the diff would have surfaced: a tint prop was meant to layer on top of a tone, but both supply a background color, and CSS resolves the conflict by stylesheet order, not by the order the two class names were concatenated in the source. The code read correctly. On screen, four of five category tiles rendered with no tint at all. It was only visible by actually rendering the component against the real compiled CSS and comparing it to a screenshot — which is exactly how it was found, and exactly the discipline that runs through this entire project: build it, then look at it, because looking at the diff proves nothing about what a browser does with cascading styles.
Tabs that were never tabs
A table-spec PR found that tabular-nums existed in exactly one of eleven tables, so numeric columns visibly shifted on every refetch everywhere else — the precise problem one table's own code comment claimed to have already solved. A segmented-control PR then applied a sharper distinction: by the standard rule that tabs navigate between distinct content areas while a switcher toggles alternate views of the same content, five of six "tab" strips in the app weren't tabs at all — they were filters dressed as navigation, with zero ARIA semantics and zero keyboard support between them. SegmentedControl is a radiogroup, not a tablist, and the new tests pin that distinction specifically: that it renders no tabpanel, exposes exactly one aria-checked, and that a group of one option renders nothing at all, because a group of one isn't a choice.
A guard that proved it could fail, in both directions
Six PRs of unification are only as durable as the thing stopping the seventh hand-rolled button. The lint rule that landed next fails the build on any raw <button> or <table> outside four explicitly-named, explicitly-justified exceptions — and the PR's own verification is the part worth noting: a raw button and a raw table were each deliberately reintroduced to confirm the rule actually fires, then reverted to confirm it passes again. The first draft of the guard had exempted an entire directory, which also accidentally exempted the three shared components that most needed the rule to apply to them. Run for the first time, it immediately found four raw tables that six prior PRs of "done" migration work had simply missed.
Verified, not assumed
A small /api/version endpoint shipped next, deliberately public and deliberately boring, for one reason stated plainly in the PR: every "deployed ✅" claim up to that point had been an inference from the service still answering, which it would also do before a bad deploy. Within a minute of the first production deploy, the endpoint itself disproved a claim its own author had just written in the same PR — that the host exposed no git commit variable, so the field would always read null. Production returned the exact merge commit. The author had checked the wrong surface: a CLI command that lists user-defined environment variables, not the actual runtime environment a deployed process sees. The fix was a one-line doc correction, and the honest write-up of being wrong about your own infrastructure in the same week you shipped it is a better tell for engineering maturity than getting it right the first time would have been.
Two outages the migration itself caused
The riskiest part of a consolidation like this isn't the components — it's moving them across a server/client boundary that doesn't forgive a wrong guess. A route split that made every manager module independently linkable also, in the same PR, took down every single one of those routes in production: a helper function was exported from a file carrying "use client", and in the App Router, every export of a client module becomes a client reference on the server, including a destination that's a plain exported function. A server component that calls it isn't invoking a function anymore — it's holding a reference, and the render throws. Four hundred and some passing tests said nothing, for a specific, structural reason the author wrote up rather than shrugged off: the test suite renders everything as a client component, so there's no server/client seam in the test environment for the bug to cross, and the build step never executes a dynamic route at build time either. The local gate was architecturally blind to this exact class of bug.
The same mistake recurred one PR later in a different shape — a const string exported from a client module, not a function, breaking /login and all five admin panels — and the scanner written specifically to catch the first occurrence missed the second, because it only looked for called functions, and a string is read, never called. The fix wasn't the component; it was the detector, rewritten to flag any lowercase import from a client module regardless of how it's used, and to walk the actual server-reachable file graph instead of trusting the presence or absence of a directive. Worth saying plainly: the fix for /login was reported as resolved once already, on the strength of a working deploy and clean route sweep, before the root cause — a React render prop crossing a boundary it can't cross — was actually found by loading the page and reading the error.
Eighteen silent bombs
Admin was the single riskiest surface in the whole migration, with zero render-test coverage across sixteen files, because the whole area is server components with server actions that fetch on render — nothing in the test harness can mount them. Eighteen of its twenty-one buttons were bare elements inside a form relying on the browser's implicit submit-on-click behavior. The shared Button primitive deliberately defaults to a non-submitting type, specifically so a button can't accidentally submit a form — which meant migrating any of those eighteen without also adding an explicit submit type would silently stop the form from doing anything, with no error, on a diff that reads correctly line by line. The guard for this was written and proven before a single button moved. It's also where the project's most repeated lesson shows up explicitly three separate times across three different PRs: a verification script that prints a success message instead of asserting a precondition can report a clean pass while having checked nothing at all, and the fix each time was the same — stop trusting a quiet result as evidence, and confirm the check can actually fail before trusting that it can pass.
What rendering caught that the Tailwind sweep couldn't
A tracker-migration PR found a progress-bar color applied through an inline style rather than a class — invisible to every one of the prior color-token sweeps, because none of them touch inline styles, and it had survived as roughly 2.4:1 contrast the whole time. The same PR fixed a delete-confirmation dialog with no dialog role, no focus trap, and no focus return — Tab walked a keyboard user straight through it into the page underneath, and Escape did nothing. And a follow-up PR closed the gap in the lint guard itself: color had never had a checker at all, which is exactly how over a hundred raw palette classes survived an earlier "completed" pass. Once written, that checker immediately found a single UI state — "AI usage: Major" — rendered in three different colors across three different files, one of which was dead code reachable from nowhere, from a decision nobody could any longer explain.
Safari rings every clicked button, and nobody can test that from a laptop
Fixing the original missing-focus-ring bug correctly for Chrome accidentally broke it for Safari: Safari's native :focus-visible heuristic matches after a mouse click, where Chrome's doesn't, so identical CSS produced two different behaviors depending on which engine rendered it. The fix stopped trusting the browser's heuristic and tracked the last input modality explicitly, defaulting to "keyboard" so a tracking failure fails toward more accessibility rather than less. The honest limit, stated directly in the PR: there's no browser automation installed in this environment and a page script can't produce a trusted click event, so the one thing that actually needed verifying — does a real mouse click in real Safari produce a ring — could not be verified by the person who wrote the fix. It shipped flagged as needing a human to physically click a button after deploy, rather than quietly marked done.
The loading skeleton that forgot where it lived
The very next PR was a regression from the one before it: clicking into Admin or the Tracker rendered a full-bleed wall of loading bars with no sidebar and no page margins, while the one section whose layout had already been restructured during the route split looked correct. A route's loading.tsx replaces the page but leaves the surrounding layout alone — and in the two sections that hadn't been touched yet, the shared shell component lived inside the page itself, so the skeleton took the chrome down with it. The fix was caught, by the author's own account, specifically because it was verified against a running production build rather than an isolated component harness — the exact check that had been skipped the first time, and the one that mattered.
The pattern: a consolidation is a migration, not a cleanup
Every one of these production incidents happened because the consolidation was working as intended — pulling shared logic into fewer places is precisely what makes a server/client boundary violation in one of those places take down every screen that now depends on it, instead of just the one screen that used to hand-roll its own copy. The lesson that recurs across this whole arc isn't "write more tests." It's that a check which can pass vacuously is worse than no check, because it gets quoted back with confidence — and the fix, every time it happened, was the same one: before trusting a green result, go break the thing on purpose and watch the check turn red.