Design system for vibe coding: why a vibe coded app drifts, and what holds it (2026)
A vibe coded app looks finished long before it is. This is what went wrong in one of mine, why the rules I had written down did not stop it, and what changed when they became something the agent has to pass.
Press “New filament” in my print cost calculator, and the table drops 588 px. The agent had used the wrong pattern: this form should open in a drawer, not inside the page.
A vibe coded app needs a design system an agent reads, because the agent starts every session knowing only what is written down. Without tokens, defined components and rules it guesses again each time, and every page decides alone. The system also has to keep changing: each correction becomes a token, a component or a check, and rules that stopped being true come out.
| For example | Where it goes | |
|---|---|---|
| A value | “Too yellow” | A design token |
| A mechanic that repeats | “Make it a component” | A component in the library |
| Something a machine sees | “No native dropdown” | A lint rule or a hook |
| Genuine judgement | Which pattern fits this product | A sentence in the rule file, and only then |
The project
I print things on a 3D printer and I wanted to know what a print costs me. I built a calculator, Bambook, with Claude Code in a day: one HTML file, products, machines, filaments, a print log, charts. It worked, and it looked like a real product.
That is what vibe coding gives you. The result is visible first, and the decisions behind it stay invisible. This was not even the worst case. The project already had a token file and a rule file, carried over from an earlier build.
What it looks like from the inside
A vibe coded app rarely looks bad in a screenshot. The problems show when you use it, or when you ask for the next feature.
- Every page decides alone. Filaments, machines and the print log each put their form in the page on their own.
- Every fix is a new guess. The status in the filament list came back as a button in the row, then a boxed dropdown, then coloured pills. Nothing said what a status looks like, so each round started from nothing.
- The agent copies what is there, mistakes included. When I moved the app to Next.js, the goal was to keep the old behaviour exactly. So the old mistakes moved too.
- Names come from somewhere else. The tokens carried the prefix of the project they were copied from.
- Old decisions keep steering. The decision log still said the data lives in the browser after it had moved to a file.
The forms that slid down
Press “New filament” and the form opened above the table. The whole table moved down. I measured the first row before and after the click, on the old layout and on the new one, at 1440 × 900:
| Form in the page | Drawer | |
|---|---|---|
| New filament | 588 px | 0 px |
| Add purchase | 470 px | 0 px |
| New machine | 568 px | 0 px |
| Log a print | 774 px | 0 px |
I asked why
I had a rule for this. I had a component system. So I asked the agent why it was built this way. The answer:
“My fault. I carried it over from the old HTML app unchanged and never questioned it.”
- The rule existed: a click never moves anything else on the screen. It was a sentence in a file.
- The components existed, and the form was built from them. None of them said where a form opens.
- The linters ran. They check colours, sizes and native elements. They do not see a table sliding down.
- The task was wrong. Keep the old behaviour exactly, so that is what it did. It measured layout jumps on what it had changed, and never on opening a form.
What an agent needs from a design system
A person learns a product. After a month nobody has to tell a designer where a form opens. An agent does not learn: it reads what is written down again in every session, and what is not there does not exist for it.
Most design systems describe how things look. Tokens answer which blue. Components answer which button. This failure had nothing to do with looks. The question was where a form goes, and the system had no answer, so the old code answered it.
An agent needs behaviour and placement written down the same way as colour:
| Size | Where it opens | |
|---|---|---|
| A confirmation | Yes or no | Modal |
| A short form | One to three fields | Modal |
| A form opened from a list | More fields | Drawer from the right |
| Long work | You stay in it | A page of its own |
I took that table from looking at 20 screens in other products. The choice was mine. Writing it down is what makes it the system's.
Where a correction goes
When something is wrong, the easy move is one more sentence in the rule file. That is the weakest place for it, as this story shows. The forms got three things: one Drawer component, one lint rule, and one line in the rule file. A form outside the Drawer now fails:
error A create or edit form opens in <Drawer>, never in the page flow: it would push the page down. The product page is the one exception no-restricted-syntax
All four forms open from the right, and the first row moves 0 px.

The rest of the app grew the same way. “Too yellow” became a colour token. “Make it a component” became one floating surface, used by both the toast and the bulk action bar. “Not the native dropdowns” became a lint error. A correction leaves something behind, so it no longer rests on anyone remembering it. If the same correction comes twice, the rule sits in the wrong place: move it, do not repeat it.
The rules go out of date too
A rule file is code, and it gets bugs. A review found two token names that no longer existed, a rule that contradicted another one, decisions that were no longer true, and a token name check that was switched off. Nobody saw it, because nothing reads a rule file except the agent, and the agent believes it.
So refining means removing as well. Mark what was replaced. Delete what nothing uses. A check found 96 unused strings in the translations when one file went over the 500 line limit and blocked a commit. Nobody had asked for that cleanup.
What this does not do
- It does not catch what nobody has thought of. The Drawer rule knows components named Form or Editor. The test that measures layout jumps on form open still runs outside the repository.
- It does not replace taste. The agent tried three status designs. I picked the fourth, from Linear.
- It costs something. The agent reads before it works, and the rules take time to keep up.
- It does not end. New kinds of failure keep coming, which is the reason it has to keep changing.
Start with the rules
I added all this after the mess. On the next project I want to start from it: rules, tokens and checks taken from a project where they held up, reviewed at the start, then cut and extended as the work goes. Not written from zero, and not as a long document nobody follows.
In short
An agent reads only what you wrote where it looks, in every session, so a decision that is not there gets guessed. A sentence in a rule file does not stop a mistake. A token, a component or a check does. Put each correction in the strongest place it fits, take out what stopped being true, and expect the system to keep changing.
This is what an agent-ready design system is made of. How it works in an existing product: Agent-ready design system