Mass-produce web tools with an AI agent and you will hit this wall: the design drifts on every generation. Same instructions, subtly different colors, spacing, and component shapes each time. Running 600+ tools in five locales, I went through three distinct eras of trying to lock the design down. Each era had a failure worth writing down.
Era 1: define the UI in JSON → it locked. And then
First attempt: define each tool's UI as JSON and generate the page from it. Scope: simple calculator-type tools with no image preview. Labels, inputs, outputs as JSON values, rendered through a fixed pipeline.
As a locking mechanism, it worked. What's in the JSON is what renders — generation-to-generation drift: gone.
But the screens came out lifeless. Straight lines everywhere, not a curve in sight. Every value sitting in its own little cell. To put it bluntly: it looked like a spreadsheet.
In hindsight the cause is structural. In a JSON-definition scheme, the expressive ceiling is whatever the renderer implements. You can add rounded corners and gradients and exception paddings to the schema — but that means endlessly fattening the schema and the renderer, and the limit of that road is reinventing HTML and CSS.
The mechanism that killed the drift killed the expressiveness with it.
Update (Aug 2026): the actual Era 1 JSON, and the screen it produced
A commenter asked to see the actual Era 1 JSON — how exactly the structure/renderer split worked before the token-only locking. I dug the files out of the January 2026 commits.
First, the screen. This is the Era 1 BMI calculator, re-rendered today from the code exactly as it existed then (English locale, 170 cm / 65 kg entered):

This is the screen I meant by "looked like a spreadsheet."
The entire UI definition for that tool was this JSON — the real catalog entry (ja/es locales omitted):
{
"slug": "bmi-calculator",
"template_id": "calc/bmi_calculator.html",
"category": "calc",
"locales": {
"en": {
"name": "BMI Calculator",
"desc": "Calculate Body Mass Index.",
"headline": "BMI Calculator",
"label_height": "Height",
"label_weight": "Weight",
"btn_calc": "Calculate",
"result_prefix": "BMI:"
}
}
}
That's it. Labels and strings. No colors, no spacing, no layout. Structure, styling, and the calculation logic all lived in the fixed 53-line template that template_id points to — the JSON values just get poured in:
<div>
<label class="block text-sm font-medium mb-2 ...">{{ t.label_height }}</label>
<input type="number" id="height" placeholder="cm" class="w-full p-2 border rounded ...">
</div>
<button onclick="calculateBMI()" class="w-full bg-indigo-600 ...">
{{ t.btn_calc }}
</button>
The AI could only ever fill in JSON values, so drift was structurally impossible. And the screen could never express anything the template didn't already implement. The one-value-per-cell look in the screenshot is the direct consequence of that split.
Era 2: lock design tokens, not layout
Change of policy: stop locking structure (layout); lock only identity (the system of color, typography, radii, spacing).
Concretely: a CSS variables file becomes the canonical source, and every tool uses only those variables (palette, fonts, radii, shadows). Alongside it, a living UI kit — a reference implementation you can actually open in a browser. "This is what this project's card/button/input looks like," expressed as running HTML rather than a spec. The instruction to the AI changes from "render exactly this JSON" to "use this kit's parts and variables; compose the layout freely."
The result: layout optimizes per tool while the brand stays coherent. It's Era 1 with the locking granularity inverted. Era 1 locked everything and expression died. Era 2 locks tokens only and composition is free.
One tailwind helped: the models' own design ability kept improving with each release. "Compose freely" only works when the compositions come back good. Era 1's premise — "free composition means drift" — was true at the time, and then time dissolved the premise.
Era 3 (now): ride the official rails where they exist
Today, skill mechanisms in Claude Code and official design-system integrations cover a growing share of "teach the AI your design conventions." What's left to self-build is shrinking to the genuinely project-specific parts: your tokens, your UI kit.
What transfers out of this
First: "it locked" is not the same as "it succeeded." Evaluate the locking mechanism including its side effects. Era 1 achieved its goal and failed.
Second: choose the granularity of what you lock. Lock down to layout and you get spreadsheets. Lock tokens only and brand coherence coexists with compositional freedom.
Third: put an expiry date on your premises. "Free composition means drift" was a fact, and then it wasn't. When a mechanism's premise collapses, throw the mechanism away.
Limits and caveats
Era 1's JSON approach isn't universally wrong. For outputs where structural uniformity is the value — reports, forms, genuinely spreadsheet-like artifacts — JSON definition is still the right answer. My case was web-tool UI, so it wasn't.
This whole arc ran in parallel with 2025–2026 model improvements. The optimal locking granularity will keep moving as model capability does.
Timeframe: H1 2026 (the Era 1 JSON catalog still exists in a March 2026 backup). Environment: Claude Code / 600+ web tools × 5 locales.
Top comments (5)
This reminds me of a broader engineering principle: constrain invariants, not implementations. The things that define your product identity—design tokens, accessibility rules, interaction contracts, brand semantics—should remain stable. Everything else should be free to adapt to the context. The same idea appears in APIs, distributed systems, and domain modeling: over-constrain the implementation and you kill evolution; under-constrain the invariants and you lose consistency. Great write-up!
Thank you for the kind words! I don't get complimented very often, so this really made my day.
I’m glad it did. 😊 And honestly, the update made the point much stronger because it showed the trade-off instead of just describing it.
What I liked most is that you didn’t present Era 1 as a mistake—you showed it as the right solution for the wrong abstraction level. That’s a distinction a lot of engineering write-ups miss.
Looking forward to seeing how this evolves as models get better.
Great write-up, and I really recognize this problem — design drift when scaling AI-generated UI is real pain. Would love to see 🤔 a concrete example of the Era 1 JSON schema you used (even a minimal calculator snippet) to better understand how the structure/renderer split worked before you moved to token-only locking. Thanks for sharing the journey through all three eras!
Thanks for the comment! I dug up the actual files and added an update to the Era 1 section — the real JSON catalog entry, a template excerpt, and a screenshot re-rendered from the January code. Hope it helps!