When AI Writes Your UI: Reviewing for the Design System It Doesn't Know You Have

AI assistants write plausible components at astonishing speed — and quietly invent a second button, a third modal, and a parallel spacing scale. How to review UI for drift.

SaveSAVEsavethree of themSavedesign system
Jim
Senior Frontend Engineer
Jun 8, 2026
6 min read

We found the fourth button implementation on a Tuesday. Not the fourth button — the fourth Button. We had <Button> from our design system, battle-tested, with focus rings, loading states, and four years of accessibility fixes. And then, sprinkled across three recent PRs, we found: a <button className="px-4 py-2 bg-blue-600 rounded..."> with hand-rolled Tailwind, a new PrimaryButton.tsx that duplicated ours at 80% fidelity, and a styled <div role="button"> (no keyboard handler, naturally).

All three had arrived via AI-assisted PRs. All three worked. All three had passed review, because each one, viewed in isolation, looked like perfectly reasonable code.

That's the new failure mode in a nutshell. AI assistants don't write bad UI code — they write plausible, self-contained, convention-free UI code. And self-contained is exactly the problem, because a design system's entire value is that components are not self-contained. They're shared.

Why generated UI drifts

It helps to understand why this happens, because it's not carelessness — it's mechanics.

An assistant's picture of your codebase is whatever fits in its context: the file being edited, maybe a few neighbors, whatever it was shown. Your design system — the packages/ui directory, the spacing scale in the Tailwind config, the "we always use <FormField> for label wiring" convention that lives in the team's heads and in a hundred existing usages — is mostly not in the window. So when asked for a card with a button, the model reaches for the statistically average card-with-button from its training data: generic Tailwind utilities, inline SVG icons, a bespoke modal with its own backdrop div.

The result is code that would be completely fine in a fresh side project and is quietly corrosive in yours. Each generated component is a small fork of your visual language: a slightly different blue, a gap-3 where your scale says gap-4, a 6px radius among your 8s, a shadow that exists nowhere in your tokens. No single instance is worth blocking a PR over, which is exactly how you end up with forty of them.

And drift compounds. Future generated code pattern-matches on whatever's nearby — including the drifted code. Once two hand-rolled modals exist, the third becomes more likely, from humans and machines alike. Inconsistency is self-seeding.

The costs are real, not aesthetic

It's tempting to file this under "polish, fix later." I'd push back on that, on three grounds:

Users feel it. Design consistency is how an interface teaches people to use it — same button means same kind of action, same spacing rhythm means same grouping logic. Every duplicate primitive is a small crack in that teaching. One-off components are also where accessibility silently dies: our design-system Button had focus management and aria-busy; the <div role="button"> had vibes.

Maintenance multiplies. When the brand color changes or a focus-ring bug is fixed, the design system updates in one place — and the forks don't. Every duplicate is a place future fixes won't reach.

It rots the system itself. A design system survives on the assumption that it's the way UI gets built. Once enough parallel implementations exist, that assumption dies, and with it the leverage.

Reviewing for drift: what to actually look for

The good news: this is a very reviewable class of problem, once you know the tells. My checklist when a PR adds UI — and doubly so when it's AI-assisted:

  • New files that shadow existing primitives. Any diff adding Button.tsx, Modal.tsx, Spinner.tsx, Badge.tsx, Tooltip.tsx should trip an alarm: 95 times out of 100 that component already exists. Search before approving. This is the single highest-value check.
  • Raw HTML where a system component exists. A bare <button>, <input>, <select>, or <table> with a pile of utility classes, in a codebase that has wrapped versions of all four.
  • Off-scale values. Hardcoded hex colors instead of tokens, arbitrary values like p-[13px] or text-[15px], magic z-indexes, one-off shadows. Generated code loves arbitrary values because it doesn't know your scale exists.
  • Parallel patterns. A second toast mechanism, a hand-rolled dropdown next to your Radix/Headless-based one, fetch in a component when the codebase uses a query layer, inline SVGs when there's an icon package. Not just "is this code good?" but "does this match how we already do this?"
  • Vocabulary drift. Props named variant="main" on a system where everything is variant="primary"; size="big" amid size="lg". Small, but it's how conventions dissolve.

A meta-tell worth naming: generated code often looks conspicuously complete — thorough, commented, self-sufficient. In UI code, self-sufficiency is frequently the smell. The right diff for "add a confirmation dialog" in a mature codebase is usually small, because it mostly composes things that exist:

// Suspiciously self-contained (drift):
// 120 lines: backdrop div, useEffect for Escape, focus trap attempt,
// hardcoded colors, its own button styles...

// What it should be in this codebase:
<ConfirmDialog
  title="Delete invoice?"
  confirmLabel="Delete"
  tone="danger"
  onConfirm={deleteInvoice}
/>

When a UI diff is much bigger than the feature warrants, ask what it's rebuilding.

Fixing the pipeline, not just the PRs

Review is the safety net, but you can reduce how much lands in it:

  • Feed the assistant your conventions. Most tools read a project instructions file (CLAUDE.md, .cursorrules, etc.). A dozen lines — "always use components from @acme/ui; never hand-roll buttons, modals, or form fields; spacing comes from the theme scale; icons from @acme/icons" — measurably changes what gets generated. Point at real example files; assistants imitate what they're shown.
  • Make the right thing greppable. Good barrel exports, a components README, consistent naming. What's easy for a new engineer to discover is easy for a model to be shown.
  • Lint the drift you care about. Ban raw hex values and arbitrary Tailwind values via lint rules; restrict imports of styled-components or raw <button> in app code if you're serious. Machines are great at holding lines humans get tired of holding.
  • Be gracious in the comment. The author often didn't choose the duplicate — they accepted a suggestion that looked right. "We have <ConfirmDialog> for this — swap it in? (Assistant probably didn't know)" lands better than treating drift as negligence, and it teaches the human for next time.

The reviewer is the design system now

Here's how I've come to think about it. An AI assistant is like a talented contractor who's never been in your building: skilled, fast, and completely unaware of where anything is kept. It will build you a beautiful new door without checking that you have a doorway standard — unless someone who knows the building is checking the work.

That someone is the reviewer. The craft questions haven't changed — does this compose what we have? does it speak our visual language? will a future fix reach it? — but the volume has, and the code arrives looking more polished than its integration actually is. Approve the plausible-but-parallel version often enough and you won't have a design system anymore; you'll have a museum of ways buttons used to be made.

Search for the primitive before you approve a new one. Ask why the diff is bigger than the feature. Teach your tools your conventions. Consistency was always a team sport — it's just that one of the players now needs the playbook written down.

ai-assisteddesign-systemscode-reviewcomponentsconsistency
Written by
Jim
Senior Frontend Engineer · Firetrail review team
More Frontend