
How to build UI with AI agents so that "looks done" actually means "done" — and why, even then, verification stays part of the job.
Anyone who already builds UI with AI knows the scenario. You give the agent a link to a Figma layout, type "build this screen" — and twenty minutes later you have a page that, at first glance, looks just like the design. You show it to the client — "wow, already?". It feels like 80% of the work is done and the rest is half an hour of polish.
A week later QA sends back fifteen tickets. The spacing is off. The color is "slightly different". A card breaks on mobile. The banner is blurry. A tab flickers when you switch. After tapping "Send" you get "Done", but the screen doesn't update.
The problem isn't that the model is bad. The agent optimizes for "looks done", while a product is judged by "matches the design and works". Between those two states sit exactly the 20% that separate a product from a demo. Better models shrink that gap, but they don't close it — they just make it less visible.
This article has two parts. First — how to build with agents so the tail is as small as possible. Then — why, even with the right process, the tail remains, and how to catch it systematically.
Part 1. How to build UI with agents effectively
Why "just give it the Figma" doesn't work
The quality of AI-built UI depends less on the model than on two things: what data the agent receives and what rules it works by.
An agent that only sees a screenshot of the layout does what a junior without dev-mode access does: it guesses. Spacing "about 12", font "semi-bold", color "blue like in the picture". On a screenshot, the difference between a 12 and a 20 gap, or between Bold and ExtraBold, is barely noticeable. In the product it produces a "something's off" feeling that's hard to name but easy to notice.
Structured context is a different story. Through dev mode or an MCP integration with Figma, the agent receives data instead of a picture: color tokens, auto layout with exact gaps and padding, icon names, verbatim copy, component variants. Then it doesn't guess — it transfers.
That's why the first rule I put into my working instructions for the agent is: no Figma context — no code. A screenshot is only for the final visual comparison, never a source of values. The rule didn't come out of nowhere. More than once, the agent decided on its own — with no instruction to do so — to build screens from a screenshot, presumably to save time: "it's all obvious anyway". The result looked plausible, and afterwards I had to do a full audit against the real layout data, fixing even the letter case in the copy. Now it's explicitly forbidden.
Preparation: the design system before the first screen
The cheapest place to fix a hundred future mismatches is before the first screen is built.
Tokens first. Colors, typography, radii, spacing — extracted from Figma into the project theme (globals.css, a theme file) before the first line of markup. And a hard rule: no literals in the code. Not #1E96FC and not px-[13px], only var(--color-primary) and the spacing scale. An agent that's allowed to write literals will inevitably write an "almost right" shade.
A component library before pages. Button, badge, input, card, tabs, bottom sheet — with every state: default, hover, pressed, disabled, loading, error, empty. Pages are then assembled from verified building blocks instead of being built from scratch each time.
Two agents checking each other. One builds a component from a Figma node. The other — independently, with the same nodes — looks for discrepancies. The loop continues until the reviewer says "approved". At the library stage this pays off especially well: a bug in the base button multiplies across every screen.
Storybook or an equivalent as a contract. Every component and every state is visible in isolation. Storybook screenshots become the baseline for visual regression: if tomorrow someone "slightly tweaks" the button, the test will show it.
A node registry and a spec cache. I keep a file mapping "screen/component → Figma node-id" and cache Figma responses in the repository. The agent doesn't fetch the same node every time, and the team has a single source of truth.
The process for building one screen
Building with an agent isn't a single request — it's a pipeline of phases. Mine looks like this.
- Find the exact nodes. Not "the whole page", but the smallest meaningful node — a card, a row, a badge. Requests run in parallel.
- A spec table before code. For every element — color, background, border, radius, padding, gap, font, icon, text. If a value is missing from the response, the agent doesn't invent it; it requests a narrower node.
- Code — one spec row, one class. No extra wrappers and no "just in case" spacing. Copy is taken verbatim from the layout, including case and quotation marks.
- Five separate self-review passes: colors → sizes → typography → icons → copy. Separately — because "check everything" in practice means "checked nothing in particular".
- Verification on three levels. Desktop and mobile screenshots, a comparison against the spec, and computed CSS — the values the browser actually applied, not "what the classes should have produced". The main rule of this stage: don't tweak values to match the picture. If the screenshot doesn't match — re-read the node instead of fiddling with numbers at random.
- Code review by separate agents with a quality bar. If it falls short — another iteration.
Between phases — a short report. Long silent agent sessions are the perfect environment for something to quietly get lost.
A goal instead of a prompt: a tool worth trying
One idea that follows from everything above is to give the agent not the task "build the page" but a goal with criteria that can be verified. Part of this can be done with an ordinary prompt: just spell out at the end what counts as "done". But Claude Code also has a dedicated mode for it — the /goal command. I haven't tried it in my own work yet, so this isn't a recipe — just a tool worth a look.
The idea is that instead of an instruction, you set a completion condition:
/goal the leaderboard screen matches Figma node 15162:20481 at widths 393 and 1440;
it has "empty", "error" and "loading" states; nothing is clipped with an enlarged
system font; flutter analyze and all tests are green
According to the description, the agent then works on its own, and after each of its turns a separate evaluator model checks the condition and returns a verdict: "not yet" (with an explanation of what's missing), "done" or "impossible". You can check the status with /goal without arguments and clear the goal with /goal clear. If there's no progress for several turns in a row, the mode stops and hands control back to a human.
Why it's potentially interesting: the "done" decision is made not by the same agent that wrote the code but by an external check of the condition. It's the same idea of separating roles discussed in the second part, just built into the tool. Whether it actually works better for UI than an ordinary prompt plus a separate reviewer agent — I don't know yet.
If you try it, a couple of rules probably apply:
- Criteria must be verifiable. "Nice and like the layout" is something the evaluator can't check. "A 20px gap between cards, tests green, no horizontal scroll at 320" — it can. Best of all is what can be confirmed by a machine: screenshots, computed CSS, golden tests, lint.
- One goal — one clear outcome. "Build the whole app" is vague; "screen X with these states" is not.
And an important limit: even if the tool works well, the evaluator checks exactly what's written in the condition — and the tail often lives precisely in what you didn't think to write. So it's one more way to shrink the tail, not to remove it. Why — in the second part.
Part 2. Why even this isn't 100%
The agent forgives itself
Over a long session, the agent gradually lowers the bar. Not out of "laziness" in the human sense — but because its main objective quietly shifts from "do it well" to "finish the task". And it starts turning a blind eye.
What that looks like in practice:
- "Almost like the Figma". Instead of the required component variant, the default one stays — "it looks similar, after all". A tab switcher is "glassy with a shadow", while the layout has a flat grey one.
- Data that doesn't exist. The API doesn't return some field — the agent quietly hides the element. That's correct. But elsewhere, the same agent, so the block doesn't look empty, fills in a made-up number like "+47 completed".
- Self-review with the same eyes. The agent that wrote the code reviews it with the same understanding it wrote it with. It sees what it meant to do, not what came out.
- Context fatigue. The longer the session, the more small things move into the "later" category — and "later" never comes.
The most telling case from my practice. A mobile app, twelve tickets saying "the screen doesn't match the design". Four agents built four groups of screens in parallel, and each reported: "matches Figma". After that, separate reviewer agents — with the same layout and the same nodes — sent all four groups back for rework. Between five and thirteen remarks in each. And these weren't stylistic nitpicks: among the findings was a file-upload bottom sheet where a file with an error disappeared from the screen but still counted towards the limit — meaning the user simply got stuck. Visually — perfect. Functionally — a blocker.
After two or three rounds of fixes, all groups got "approved". The conclusion is simple: this isn't a property of a particular model, it's a property of any performer who accepts their own work. The solution is to separate the roles. The builder and the reviewer must be different.
Visually done ≠ done: where the tail hides
The most common discrepancies aren't visible at first glance at a screenshot. Here's where they live.
Colors and tokens. An "almost right" shade, an 8% background opacity instead of 10%, a text color from the neighbouring token. On its own — nothing. Together — a screen that's "not quite ours".
Spacing and sizes. 12 instead of 20. A fixed height that clips the text as soon as the font gets wider. The classic flex-1 in a vertical container that quietly "eats" the given height. On paper the classes are right — in the browser the size is different. That's exactly why you need computed CSS, not faith in classes.
Image or markup. This is where the agent stumbles most often. The designer drew an illustration, a banner with text or a complex composition. A human looks at it and immediately understands: "that's an image, it needs to be exported". The agent sees layers and vectors — and tries to build them in code. The result: blurry, not responsive, falls apart at another width. It also happens the other way round: a block that should have been markup is inserted as an image — and now the text can't be translated or scaled. If the designer hasn't marked what's an asset, that decision has to be made by a human, not the agent.
Noise in the layout itself. Sometimes the problem isn't the agent but what it receives. On one project the designer made a decorative background — a grid of small circles — not as an image but as live elements. On screen it's just a background. But when I gave the agent a link to a Figma node, the MCP returned hundreds of those circles along with the meaningful layers. The context got flooded with noise, and the agent simply couldn't retrieve all the elements of the screen it needed. For a long time I couldn't figure out what was wrong, until I worked out what was getting in the way. It was solved with a separate script that stripped the decorative layers before the context was extracted. The takeaway: if the agent "can't see" part of the layout, first check what it actually receives — and agree with the designer that decorative backgrounds are delivered as images.
Responsiveness. The layout is drawn at 393 and 1440, while real screens are 320, 1024, a tablet. Plus system settings: I had a case where a leaderboard card with a fixed height broke with an enlarged system font. The layout has no such state — so the agent never checked it.
States. An empty list, an error, loading, "the backend didn't return the field". Layouts often show only the happy path — and the agent honestly builds only that.
Copy. Letter case, quotation marks, grammatical case and plurals. In Ukrainian we got "Feedback from Viktoriia" with the name in the wrong grammatical case — because the API returns the name only in the nominative. And "Up to 1 points" — because nobody thought about plurals.
Interaction. Hover, focus, swipe, animations. One of my funniest cases: when switching tabs, the neighbouring tab "flickered". The cause — a background animation from white to "transparent", where transparent actually meant transparent black, so the intermediate frames were grey. The first fix removed the grey, but for three more frames two white backgrounds were shown at once. You could only see it by recording a video and stepping through it frame by frame. No screenshot will ever show that.
Behaviour behind the facade. The "Done" toast appears, but the screen doesn't update. The cause had nothing to do with the markup: the object that was supposed to trigger the data refresh was destroyed before the request finished. The user sees "success" and doesn't see the result.
Platforms. iOS and Android behave differently. Instagram's and Telegram's in-app browsers don't open some links. Dark mode, system dialogs, autofill — things that fundamentally can't be checked without a real device.
How to close the tail systematically
If the tail is inevitable, finding it has to be part of the process, not a reaction to tickets bounced back by QA.
- Separation of roles. Builder → independent reviewer → fixes → another review. Two or three rounds are the norm, not a failure.
- Machine checks. Golden screenshots of components, a computed CSS dump, a lint rule that forbids literals instead of tokens. Everything that can be checked automatically should be checked automatically.
- Real devices for what can't be emulated: deep links, system sheets, autofill, returning from other apps.
- A checklist before "done" — a short reminder for both the agent and the human of what to check before handing over (see below).
- A human as the final filter. Fifteen minutes of a designer's or developer's careful look at the finished screen catches what all the agents missed combined.
Conclusion
Better models shrink the tail, but they don't remove it. "80% from a single prompt" will become "90%" over time, and the last ten percent will still be work — just less visible, and therefore more dangerous.
AI doesn't change the amount of work, it changes where the time goes: less on typing code, more on setting goals, verification and decisions the agent can't make on its own. The most useful thing you can do is build a process in which the agent can't say "done" until it's been verified. Not out of distrust of AI, but for the same reason we do code review for people.
Appendix: a checklist for building UI with an agent
Before code
- Exact node-ids for every screen and component, not "the whole page".
- Structured context from Figma, not just a screenshot.
- Tokens in the code before the first line of markup.
- Base components with states — built and verified.
- Decided what's an image (asset) and what's markup.
- Decorative backgrounds made of many elements are removed or replaced with an image before the context is extracted.
During
- A spec table for every element.
- No color or size literals.
- Copy verbatim from the layout.
- No data — the element is hidden, not invented.
Verification
- Separate passes: colors → sizes → typography → icons → copy.
- All widths from the layout plus 320 and tablet.
- States: empty, error, loading, offline, long texts.
- Large system font, dark mode.
- Interaction; animations frame by frame if something "flickers".
- Computed CSS for critical elements.
- An independent reviewer — until "approved".
- A real device for what can't be emulated.


