Skip to content

Twice is too late

Card for this note, reading Twice is too late, set over the note's own artwork.

A new report says half of surveyed designers shipped AI-generated code to production this year. The comments split neatly into two camps: "designers are engineers now" and "the sky is falling". Both camps are staring at the code. The code was never the risk.

I am in the half that shipped. The part worth talking about is not the code. It is one boring-sounding rule about what is allowed into the design system, and why the rule every team already uses stops working the moment you go fast.

What I shipped

In August I converted a finished design for a business operations platform, more than 190 screens, into a running application myself. Design and frontend were one activity, performed by one person in one session, with no separation between deciding and building. Ten days, fifteen modules.

At that pace, a design system is either the thing holding the product together or the first thing to quietly fall apart.

The rule everyone uses

Almost every team has some version of the same rule of thumb: extract a component the second time you need it. Build it inline once, and when it shows up again, promote it into the system.

It is a sensible rule. It avoids speculative components nobody uses. And it relies on an assumption nobody says out loud: that there is a gap in time between the first use and the second, and somebody notices the duplication in that gap.

Building at AI speed removes that gap. A second screen that needs the same thing can be built the same afternoon. By the time there are two of them, they have already diverged, one with a slightly different padding, one with a slightly different state, both committed, both looking correct. Nobody is reading the diff, because the person who would have read it is the same person moving fast.

Twice is too late.

The rule I used instead

Nothing enters the product that the design system does not own. A screen never invents a visual element. And the test for whether something becomes a component is not "has this been used twice", it is "would a second screen actually want this".

That moves the trigger earlier. If the answer is yes, it goes into the system first and the screen uses it from there. If the answer is no, it is genuinely local and stays that way, knowingly.

You can see the rule working in what grew when. Eight objects entered the design system with the sales module. Five with marketing. Five new ones plus six extensions to existing components with the agents module. Every one of those could have been a literal value inside a screen's stylesheet instead, and each of those literals would have been a design question answered silently, by nobody, and shipped.

By the end, the system held 126 documented components in 29 families, and the screens stayed thin.

The reference is the product

The second half of the rule is where the system lives. It does not live in a document anyone can quietly disagree with. The design system reference ships as a live route inside the application itself, rendering the actual components.

The rejected alternative is the industry standard: keep the canonical system in Figma and mirror it into code. The trouble is that any reference which is not the thing it describes will eventually disagree with it. With no handoff, there is no review step where that disagreement gets caught. Rendering the real components removes the possibility instead of managing it.

Two structural calls backed that up. There is no utility CSS framework in the project, because its own spacing, radius and colour scales would sit beside the ones extracted from the Figma and drift. A token value exists in exactly one folder that everything else imports by reference. And icons are drawn only through a semantic registry that binds size, stroke and colour to the system, so an icon cannot be dropped in at whatever size looked right that day.

What a running interface found that Figma never did

Building the design instead of specifying it changed what got designed. The dark theme exposed an invisible headline, a white tile on every empty surface and a hydration error on every page, none of which a side-by-side comparison with a static frame could ever have shown. Every overlay had to learn how to close, because nothing in a static file ever draws an exit. And nineteen committing actions turned out to need a confirmation toast that nothing in the Figma drew at all.

Each of those went into the system, not into the screen that surfaced it.

What it cost

Moving the trigger earlier costs something real. It means building a handful of components for a single consumer, work a stricter wait-until-you-need-it-twice rule would have skipped. Some of those components may never get their second screen. I paid it anyway, because the alternative at this speed is two versions that have already disagreed before anyone noticed.

And I want to be exact about the safety net, because I would expect the same from anyone else. The frontend has no unit test suite. It has strict types, lint and build checks across every workspace, and two purpose-written scripts: one guarding a brand boundary around the assistant components, and one failing the build if the design system's changelog stops matching its component catalogue. That net is aimed at the failures this workflow actually produces, which are drift and invention rather than logic errors. It is still narrower than tests, and I would not run a project with a serious data layer this way.

The shift the statistic misses

The report's number is interesting and the conversation around it is mostly about who is allowed to write code now. That is the least important part.

Building a screen is now the cheap part. So the judgement that matters moves to deciding what not to build and what is allowed in, and the tooling gives you no help with that whatsoever. Going up a layer of abstraction does not remove judgement. It moves all of it earlier, into rules that fire before the model gets a chance to guess.

Written September 2026.

All thoughts