I need to own something. I almost merged a button whose focus ring came from a token file that didn't exist anymore.
The component looked perfect under a mouse. But the moment a keyboard user hit Tab, the ring was gone.
Okay, I didn't actually ship it. I set up a disposable repo to see how easy this mistake would be to make. Turns out: embarrassingly easy.
The feed this week kept bringing up watermarked text, agent gatekeepers, and people building tool permissions. Those are real problems. But the less dramatic problem is happening right next to them: free generation is letting us regenerate small UI pieces so many times that they quietly stop matching the design system.
That's the thing I wanted to test.
MonkeyCode is an open-source platform that advertises free model access, a 30-million-token allowance, and a free server option at the time of writing. Disclosure: This article was prepared as part of MonkeyCode's product outreach. I haven't verified whether those limits are permanent, so check the current terms before you build anything around them.
Here's the part I didn't expect. The tokens were not the cost. The cost was drift.
The free-layer drift loop
Free generation changes your behavior without changing the model. When every regeneration costs nothing, you stop treating a component as a decision and start treating it as a lottery.
First version: close enough.
Second: closer.
Third: I think the padding changed.
Sixth: now the focus ring source is gone and everything still looks okay at a glance.
The system prompt didn't change. The token contract didn't change. Only the number of attempts went up. That's not a model problem. It's a repetition problem with no version pin.
My drill started with one button. I asked the agent to regenerate it five times from the same brief. By the third output, the spacing inline value had shifted from space.4 to space.5. By the fifth, the focus ring came from a hand-tuned hex value that wasn't in the token file at all.
A mouse user would never notice. A keyboard user would.
A component contract is not a prompt
A prompt that says "use the design system" is not a check. It's a wish. What worked was writing down the exact token references the component must use, and then checking the output against that list like a diff.
Here's the tiny contract I used in the drill:
{
"component": "primary-button",
"tokens": {
"background": "color.action.primary",
"focus-ring": "focus.ring.primary",
"spacing-inline": "space.4"
},
"source": "tokens/core.json",
"allowed_edit": "none"
}
allowed_edit: none is the part that matters. It says the model can regenerate the button, but it cannot quietly swap one token source for another. If the output needs a different spacing value, that has to happen in tokens/core.json, not inside the component.
That tiny rule separates a design decision from a generated artifact. Most drift I see happens because both live in the same file until nobody can tell which one was approved.
Add a design review card
After a few regenerations, I stopped reading the full diff and started checking only three things. That's what a review card is: a fixed set of drift checks that survive the free-token lottery.
| Check | Look for | Success | Stop if |
|---|---|---|---|
| Token source | Every style value resolves to a key in tokens/core.json
|
All values match the contract | A raw hex or spacing value appears where a token should be |
| Focus ring |
:focus-visible style still points to focus.ring.primary
|
Visible ring on keyboard Tab | The ring is removed, recolored, or turned into outline: none
|
| Spacing | Inline spacing value equals the pinned token | The component uses space.4
|
Two regenerations in a row use different spacing values |
That last row is special. One spacing change is noise. Two spacing changes across attempts is drift. The free server gives you the space to notice the second one before it ships.
Run a drift check, not a speed test
The free token allowance tempts you to measure throughput: how many components can I get? I found it more useful to measure drift: how much did the contract change after N regenerations?
Here's the check I ran against the generated component:
def drift_errors(component, contract):
mismatches = []
for prop, token_key in contract["tokens"].items():
value = component.get(prop)
if value != token_key:
mismatches.append((prop, value, token_key))
return mismatches
# Run this after every 5 regenerations
# Success: zero mismatches
# Stop: any mismatch that repeats on the next attempt
This is pseudocode and unexecuted. Treat the stop condition as a hypothesis, not a benchmark. The useful part is that it makes the drift explicit instead of letting a visual review decide that the button still looks fine.
Accessibility: the focus ring goes first
The focus ring is always the first thing the agent drops. Not because it's the least important, but because it's invisible to a mouse-based review. If your check is "does it look right," you are optimizing for people who don't use a keyboard.
The accessibility fix is not to write a longer prompt about inclusion. It's to make the drift check keyboard-first. Before you review the visual output, hit Tab. If the focus outline is missing or shifts to a hardcoded color, that's a contract failure even if the button looks polished.
For designers and frontend developers, this is the part that matters most. The free tier's real risk isn't bad code. It's a hundred plausible-looking buttons that each deviated a little from the accessible source of truth.
Recovery when the token file starts lying
Here's the recovery path I keep in the record.
If a component drifts:
- Do not hand-fix the component file and move on.
- Revert the component to the last version that matched
tokens/core.json. - Decide whether the token file itself needs to change.
- If yes, update the token file first and then regenerate the component from it.
- Add a regression test for the exact property that drifted.
That order matters. If you fix the button first, you've created a secret exception. The next agent that regenerates the button will have no way to know it was supposed to stay different.
Keep the old token file in the task record. Don't overwrite it. The record should show which property drifted, which version it drifted from, and which version the human approved. That makes the drift reversible without erasing the evidence that it happened.
Who should skip this
Skip the drift check if you don't have a token file or a shared design system. In that case, there's no source of truth to drift from, and this whole setup is just overhead.
Skip it for visual explorations where consistency doesn't matter yet. A moodboard can drift all it wants. A button in a shipped UI cannot.
Skip it if you're the only person looking at the code and the cost of a broken focus ring is low. But if another designer or developer might build on top of your component, the drift will compound.
The free tier should buy you a drift log, not more buttons
A generous free allowance feels like an invitation to generate more. The better invitation is to use the free server to run a repeatable check.
Try it this way: pin a token file, generate one component five times, and run the drift check after each round. Count the mismatches. If you see two or more, you've found the real cost of the free server.
It's not the tokens. It's the quiet moment when the button still looks right and no longer is.
Top comments (0)