Back to writing

Writing / AI Engineering

Can copy-paste beat code generation?

I built a custom test harness to compare three ways AI agents build the same interface. Across 88 runs, which approach produced the most reliable UI?

DateAug 13, 2026
Read time13 min read
TagsAI Agents · Evaluation · Frontend Engineering

Can copy-paste beat code generation?

Can copy-paste beat code generation?

Executive Summary

The short version

Copy the known parts. Design and test the joins.

Across 88 runs, copied component source was the strongest starting point. It was not a substitute for product-level composition.

1Arm C
Copy vetted source

The model writes the product-specific glue.

0 build failures
2Arm A
Generate directly

Reliable, but more bespoke interaction code.

0 build failures
3Arm B
Read, then recreate

The model rewrites a component that already exists.

5 build failures
The surprising failure was not inside the copied component. It was where the application connected it.

For a designer, the useful lesson is not “copy-paste is always better”. It is that a mature component can carry colour, focus and keyboard behaviour into a product without asking the model to remember them. The risk moves to the joins: the product-specific code that connects those components into a flow.

The study covers 88 runs across eight interface tasks. Arm C, which copied vetted component source and generated only the connecting code, ranked first. Arm A, which generated the interface directly, came second. Arm B, which read the real component API and then recreated the component, came last.

Three ways to build the same screen

A component is a reusable interface part, such as a button, field, menu or dialog. Its API is the rulebook that tells other code how to use it. A primitive is a foundational component that other interface patterns build on.

The experiment gave one AI model the same product task and the same empty project, then changed where the component source came from.

Arm A — generate everything

The model writes the whole interface directly, mostly with ordinary HTML elements and local styling.

Arm B — read, then recreate

The model reads the real component documentation, but still rewrites the component source itself.

Arm C — copy, then compose

A tool copies the vetted component files exactly. The model writes only the product-specific code that joins them together.

I call that copying step materialisation: known source becomes a real project file without passing through model output.

Figure 1

Three source paths create two useful comparisons

The model, task prompt and component-free starting template stayed the same.

Arm AGenerate everything

Model weights → all interface source

Arm BRetrieve, then generate

Registry API → model recreates every line

Arm CCopy, then compose

Registry source → CLI copies components; model writes the glue

A → BWhat retrieval changes
B → CWhat copying the source changes
A to B isolates the effect of retrieving the real API. B to C isolates materialisation: whether the model recreates the component source or a CLI copies it intact.

The missing middle path changed the answer. The first version of the study compared only A and C. Arm A had no external component API to misunderstand; arm C received the correct files intact. Neither was exposed to the predicted error.

Arm B created that condition. It knew the real API and still had to reproduce every line. Five of its 16 projects failed to build. The defect was real, but narrower than expected: it appeared when the model recreated a known component contract, not whenever it generated an interface.

Before looking at the numbers, compare the screens. These three compound interfaces all passed the five browser behaviours named in the task.

Arm A compound-state interface. It uses native elements and makes more local presentation choices.

Arm A compound-state interface. It uses native elements and makes more local presentation choices.
Arm A compound-state interface. It uses native elements and makes more local presentation choices.

Arm B compound-state interface. It uses a component layer recreated after reading the API.

Arm B compound-state interface. It uses a component layer recreated after reading the API.
Arm B compound-state interface. It uses a component layer recreated after reading the API.

Arm C compound-state interface. It composes copied registry components.

Arm C compound-state interface. It composes copied registry components.
Arm C compound-state interface. It composes copied registry components.

They look broadly similar. The important differences are mostly invisible in a static mock-up: who authored the reusable layer, which behaviours arrived with it and what happened when the pieces were connected.

A successful screen needs four green lights

The compiler is the software that checks and translates source code before a production build can ship. It catches broken imports and invalid types. It cannot tell whether a menu ignores Escape or focus disappears after a dialog closes.

Accessibility means whether people with different abilities can perceive, navigate and operate the product. An automated accessibility scan can catch some serious barriers, but it cannot judge every interaction or the whole experience.

Composition is the act of connecting components into a product. The composition seam is the boundary where a reusable component meets the page-specific code around it.

Four lenses

A green build is only the first check.

Each lens sees a different kind of product failure. None can stand in for the others.

01
Can it ship?

Compiler + build

5 runs flagged
02
Does it behave?

Browser interaction

7 runs flagged
03
Can people perceive it?

Accessibility scan

9 runs flagged
04
Did the join keep the behaviour?

Composition audit

10 findings
One arm B project passed every browser check and still could not produce a production build.
Exact evidenceWhat each scoring layer can see

A green result in one layer does not imply a shippable or usable product.

What each scoring layer can see. A green result in one layer does not imply a shippable or usable product.
LayerRuns flaggedCatchesCannot see
Compiler + production build5Broken imports, invented properties and wrong API shapesAnything that compiles
Browser behaviour7Dismissal, focus and state across routesType errors when the development server still opens
Accessibility scan9Serious and critical rule violationsBroken behaviour outside the rules
Composition audit10Useful primitive behaviour discarded by the connecting codeInterfaces without a Radix-backed primitive

The layers disagreed in useful ways. One arm B command palette passed all five browser checks even though its production build had two compiler errors. A browser-only score would have called it perfect; a user could try it locally, but the team could not ship it.

The scorers were checked against a clean reference and a deliberately broken control before the experiment. Missing evidence stayed missing rather than being counted as a pass. One of the 88 runs lacked runtime evidence, so browser conclusions use 87 runs.

The failures were concentrated, not everywhere

Most runs worked. The complete ledger shows a few clear clusters rather than a general collapse of generated interfaces.

Figure 2

The complete defect ledger

88 runs across 8 tasks. Counts are defects or failed checks, not affected runs.

TaskArm A · GenerateArm B · RetrieveArm C · Materialise
Login formNo defect foundNot runNo defect found
Users table1 contrast · 4 behaviour2 build · 1 contrast2 contrast
Kanban boardNo defect foundNot runNo defect found
Command paletteNo defect found2 build · 1 behaviour · 1 seamNo defect found
Variant matrixNo defect foundNot runNo defect found
Form errorsNo defect foundNo defect foundNo defect found
Compound state11 contrast1 build · 6 contrast · 4 seam4 seam
Dashboard1 behaviourNo defect found1 behaviour · 1 seam
The predicted build failures appear only in arm B. Contrast problems cluster in two tasks. The composition seam is most visible in the compound-state task.

Five runs failed to build, all in arm B. Seven runs failed at least one browser behaviour. Nine runs had a serious or critical accessibility finding, all involving colour contrast. The composition audit found ten cases where the application discarded behaviour that a reusable component already provided.

Recreating known source caused every build failure

Arm B invented module paths, passed a property the component did not accept and twice used a real API in the wrong shape.

Exact evidenceEvery build failure in the programme

All five came from arm B: the path that knew the real API and tried to reproduce its source.

Every build failure in the programme. All five came from arm B: the path that knew the real API and tried to reproduce its source.
RunCompiler codesWhat went wrong
c1_command_palette · seed s2TS2769Passed an onClick property to a component that does not accept it.
c1_command_palette · seed s3TS2604 / TS2786Used Slot as a component when the real component is Slot.Root.
c4_compound_state · seed s4TS2604 / TS2786Repeated the same Slot namespace mistake on another task.
t3_users_table · seed s1TS2307 ×2Imported two module paths that do not exist.
t3_users_table · seed s4TS2307 ×2Imported two different module paths that do not exist; this previously unscored run was recovered and rebuilt during the manual audit.

The fifth build failure was recovered during the manual production pass. Its original snapshot had no build result, but the final source survived in the trace. Rebuilding it revealed two module paths that did not exist, matching another failed users-table run. The earlier count of four was therefore one too low.

Arm A avoided this failure by not using the library. Arm C avoided it by copying the real files. Only arm B studied an implementation that already existed and then tried to recreate it.

Copying changed where the work came from

Reading documentation was not the useful step on its own. Across the five tasks completed by all three arms, arm B cost roughly twice as much as arm A, around three times as much as arm C, wrote the most source and produced every failed build.

Figure 3

Cost and model-authored code

Mean per completed high-effort run on the five tasks tested in all three arms.

Cost per run · USD

Users table

A$0.0054
B$0.0107
C$0.0039

Command palette

A$0.0040
B$0.0089
C$0.0036

Form errors

A$0.0045
B$0.0088
C$0.0043

Compound state

A$0.0034
B$0.0116
C$0.0033

Dashboard

A$0.0123
B$0.0234
C$0.0129

Lines written by the model

Users table

A376
B673
C255

Command palette

A244
B371
C114

Form errors

A292
B491
C261

Compound state

A408
B836
C259

Dashboard

A1117
B1769
C887
Arm B is the most expensive path and writes the most source on every shared task. Arm C returns spend to roughly arm A’s level while keeping the component source out of model output.

Moving from B to C removed the unnecessary rewrite. Spend returned to roughly arm A’s level and the build failures disappeared. On the task combining tabs, a table and a dialog, arm B wrote 836 lines. Arm C wrote 258 lines and copied the existing components.

But “copy-paste writes less code” needs a qualifier. It writes less model-authored code. Copied files still ship in the repository.

Figure 4

Lines of code that actually ship

Solid segments were written by the model. Hatched segments were copied verbatim from the registry.

Users table

A376
B673
C736

Command palette

A244
B371
C357

Form errors

A292
B491
C494

Compound state

A408
B836
C922

Dashboard

A1117
B1769
C1841
Arm C authors the fewest lines and ships the most. Arm A looks lean because it usually has no reusable component layer at all.

Across the five shared tasks, arm C shipped 1.46 to 2.26 times as much source as arm A. That larger source tree contained a reusable layer that arm A usually did not create at all.

On the dashboard, arm A wrote six buttons directly in the page. Arm C copied one Button component containing variants, focus styling, disabled states and error states. Arm A did not make a smaller version of that layer; it skipped the layer.

Exact evidenceShape of the model-authored source

All completed runs. The A-to-B/C comparison is confounded by arm A’s instruction to build the interface directly.

Shape of the model-authored source. All completed runs. The A-to-B/C comparison is confounded by arm A’s instruction to build the interface directly.
ArmRunsFiles / runRuns with components folderRaw buttons + inputsRepeated class strings / 100 lines
Arm A · Generate361.45 of 364.81.63
Arm B · Retrieve166.516 of 161.80.18
Arm C · Materialise361.21 of 360.10.12

There is a fairness caveat here. Arm A was told to build the interface “directly”, while the registry arms were told where component files belong. That wording nudged arm A towards page-specific code. The clean comparison is B versus C: both were asked to create the same component layer, and differed only in whether the model rewrote it or a tool copied it.

A component file carries invisible design decisions

The original hypothesis focused on broken imports and invented properties. The more interesting design finding was what travelled inside copied source without being requested.

What travels in the file

A component is more than its visible style.

Copied source carries decisions the prompt may never mention. The application still has to connect those decisions to the right product moments.

Copied componentBehaviour that arrives intact
  • Colour and state styles
  • Escape-to-close
  • Focus-return logic
  • Disabled and error states
Product glueDecisions the app must still make
  • Which control opens what
  • Where state lives
  • How the page responds
  • What persists across routes
Copying moved stable component decisions out of the model’s memory. It did not move page-level layout or wiring with them.

The accessibility scan found only colour-contrast issues. On the compound-state task, arm A produced 11 serious or critical contrast findings, arm B produced six and arm C produced none. On the users table, arm C produced two while the other arms produced one each. The result favours C overall, but one task drives most of the gap; copying did not magically solve contrast everywhere.

Exact evidenceSerious and critical accessibility findings

High-effort runs. Every finding was colour contrast; the scan found no structural accessibility failure.

Serious and critical accessibility findings. High-effort runs. Every finding was colour contrast; the scan found no structural accessibility failure.
TaskArm AArm BArm C
Compound state1160
Users table112
All other tasks000
Total1272

Keyboard behaviour travelled more consistently. The users-table prompt never asked for its row menu to close on Escape. Arm A had to remember to add that behaviour. Arm C inherited it from the copied dropdown component.

Figure 5

Escape-to-close when the prompt never asked for it

Users-table task, split by reasoning effort. The test opens a row menu, presses Escape and checks that it closes.

No extra reasoning

Arm A
0 of 3 pass
Arm C
3 of 3 pass

High reasoning effort

Arm A
3 of 4 pass
Arm C
4 of 4 pass
Arm A’s result depends on how much reasoning it is allowed. Arm C inherits the behaviour from the copied primitive at both effort levels.

Without extra reasoning, arm A failed all three runs while arm C passed all three. At high reasoning effort, arm A passed three of four and arm C passed all four. Arm A’s result depended on how much reasoning the model was allowed. Arm C’s did not; the behaviour arrived in the file.

The join is where behaviour disappeared

Copied behaviour only helps when the application connects it correctly.

Anatomy of the seam

Same dialog. Different connection.

The component already knows how to return focus. The application can preserve that path or route around it.

Behaviour preserved

  1. Opening button
  2. Dialog trigger
  3. Dialog
  4. Focus returns

Behaviour bypassed

  1. Opening button
  2. onClick + state
  3. Dialog
  4. Focus falls to page
The composition audit found this kind of lost connection in 10 of 46 eligible runs.

The copied dialog knows how to return keyboard focus to the button that opened it. But the application still has to register that button as the opener. If it drives the dialog with separate state instead, the focus logic remains in the component file but never reaches the user.

Exact evidenceOrphaned triggers in Radix-backed interfaces

46 of 88 runs were eligible for this audit.

Orphaned triggers in Radix-backed interfaces. 46 of 88 runs were eligible for this audit.
TaskEligible runsFlaggedWhere the finding appeared
Compound state884 arm B + 4 arm C · every eligible run
Dashboard21Arm C; arm B connected the trigger
Command palette71Arm B; one arm C run repaired focus manually
Other five tasks290No finding
Total4610 · 21.7%Arm A had no eligible Radix-backed interface

Ten of 46 eligible runs lost behaviour at this seam, or 21.7%. Eight came from the compound-state task: every eligible arm B and arm C run. The defect was not peculiar to copying. It appeared whenever the dialog lived far from its opening button and the easiest implementation route bypassed the intended trigger.

These dialog screenshots look complete. All three open and close. All three return focus to the page body instead of the opening button—arm A because it never implemented restoration, arms B and C because they had the behaviour and routed around it.

Arm A dialog. It works visually, but focus is not restored after closing.

Arm A dialog. It works visually, but focus is not restored after closing.
Arm A dialog. It works visually, but focus is not restored after closing.

Arm B dialog. Its recreated component can restore focus, but the application bypasses the trigger.

Arm B dialog. Its recreated component can restore focus, but the application bypasses the trigger.
Arm B dialog. Its recreated component can restore focus, but the application bypasses the trigger.

Arm C dialog. The copied component contains focus restoration, but the application does not connect its opening button.

Arm C dialog. The copied component contains focus restoration, but the application does not connect its opening button.
Arm C dialog. The copied component contains focus restoration, but the application does not connect its opening button.

One controlled change confirmed the mechanism. Connecting arm C’s existing dashboard button through the dialog’s intended trigger moved the browser score from 14 of 15 to 15 of 15 and returned focus to the button. The same structural repair fixed the compound-state task.

The compiler, production build and accessibility scan could not see this defect. More documentation was not an obvious fix either: the model had read the dialog guidance and used the equivalent menu trigger correctly elsewhere.

Visually, all three approaches could look convincing

At application scale, the three paths converged in a four-route operations dashboard. Arm A added pill badges, chart gridlines and stronger local presentation choices. Arm C inherited structural details such as a close button that the prompt never requested. Arm B produced a similar reusable layer at greater cost.

Arm A dashboard overview, with KPI cards, gridlined charts and recent events.

Arm A dashboard overview, with KPI cards, gridlined charts and recent events.
Arm A dashboard overview, with KPI cards, gridlined charts and recent events.

Arm B dashboard overview, using components recreated after API retrieval.

Arm B dashboard overview, using components recreated after API retrieval.
Arm B dashboard overview, using components recreated after API retrieval.

Arm C dashboard overview, using copied components and a visually quieter chart treatment.

Arm C dashboard overview, using copied components and a visually quieter chart treatment.
Arm C dashboard overview, using copied components and a visually quieter chart treatment.

Copied components supplied structure. The model still made most presentation decisions. Arm A was often the most visually polished; C was often the most structurally consistent. Neither was uniformly better, and this experiment did not include a visual-quality scorer.

The row menus make that distinction tangible. All three opened and closed with Escape in the dashboard task, even though arm A had been inconsistent on the smaller users-table task.

Arm A services table with its generated row-action menu open.

Arm A services table with its generated row-action menu open.
Arm A services table with its generated row-action menu open.

Arm B services table with a model-written, library-backed row menu open.

Arm B services table with a model-written, library-backed row menu open.
Arm B services table with a model-written, library-backed row menu open.

Arm C services table with its copied dropdown menu open.

Arm C services table with its copied dropdown menu open.
Arm C services table with its copied dropdown menu open.

The arm A and C compound-state workspaces had been pruned. Their screenshots were rebuilt from durable experiment artefacts: generated files replayed from traces and copied files restored from the pinned registry snapshot. They are faithful reconstructions, not byte-identical originals.

The biggest shared design failure was responsive

The desktop screenshots hide the clearest failure shared by all three approaches.

The shared visual failure

Every dashboard kept its desktop sidebar on mobile.

At 390 pixels, the 256-pixel navigation stayed fixed and squeezed the main area to roughly 119 pixels.

Desktop
256px sidebar
390px viewport
256px
≈119px
Arm A · page scrolls sidewaysArm B · page scrolls sidewaysArm C · page scrolls sideways
A component library can supply a sidebar. It cannot decide when the product should change its layout strategy.

I reconstructed and manually browser-tested 48 matched high-reasoning outputs: 16 per arm across the five shared tasks. I also checked one representative output from every arm and task at a 390-pixel viewport.

All dashboards completed the requested filters, menus, incident flow, routing and settings persistence. All three were unusable on a phone-sized screen. Forms and command palettes fitted. Wide tables were mixed: arms B and C usually contained overflow inside a table region, while one representative arm A table widened the whole page.

Exact evidenceWhat the manual production pass changed

Forty-eight reconstructed high-effort outputs: 16 per arm across the five shared tasks, plus one representative 390px check for every arm and task.

What the manual production pass changed. Forty-eight reconstructed high-effort outputs: 16 per arm across the five shared tasks, plus one representative 390px check for every arm and task.
MeasureArm A · generateArm B · retrieveArm C · copy
Production builds16 of 1611 of 1616 of 16
Application / glue lines per run358239237
Primitive lines per run0412 · model-written450 · copied
Total shipped lines per run358652688
Dashboard at 390pxNot usableNot usableNot usable
Practical rankingSecondThirdFirst

The production pass kept the same overall ranking: C first, A second, B third. It also sharpened the code-volume claim. Arm A averaged 358 application lines. Arm C averaged 237 application lines plus 450 copied component lines, or 688 shipped lines in total.

The measurement system had defects too

The test harness—the software that ran and scored the projects—created misleading results of its own. Six defects had already distorted a number before they were found and corrected.

Exact evidenceSix defects found in the measuring apparatus

Each had already distorted a reported number before it was corrected.

Six defects found in the measuring apparatus. Each had already distorted a reported number before it was corrected.
DefectWhat it distorted
Reasoning echo counted as re-sent contextMade 23.9% of input tokens look wasteful even though the endpoint resolved that reasoning server-side at no charge.
The token guard counted cheap cache hitsStopped a tool-heavy arm C run at 627,000 tokens after only $0.016 of spend.
Task time-outs were parsed but not usedSilently ignored the longer time budgets declared by two tasks.
A forced rerun appended to an old traceCould restore files from a previous run even when the manifest described only the new one.
Run workspaces were not durableLeft 70 of 88 runs without a complete workspace; generated source was recoverable, copied files had to be rebuilt.
A cheap re-score erased browser resultsRunning the scorer without its runtime flag rebuilt the payload and deleted previously collected browser evidence.

There was a separate seventh issue in the prompt. Telling arm A to build the interface “directly” made the code-shape comparison partly a prompt comparison. It belongs beside the six harness defects, but it is not the same kind of problem: repairing it requires a new experimental arm rather than a scorer fix.

Two early checks also inspected implementation shape instead of observable behaviour. One demanded a particular input element and marked a correct component as broken. Another read an attribute and marked a menu as working without opening it. The final checks drive the interface and require a visible state change.

What designers can take into the next project

A practical workflow

Treat composition as part of the design spec.

The component contract is the starting point. The composed experience is still product work.

01Copy stable primitives

Buttons, fields, menus and dialogs with known contracts.

02Design the composed state

Show how those parts behave together, not only in isolation.

03Name the invisible behaviour

Focus return, dismissal, error movement and persistence.

04Test the joins

Keyboard, mobile layout and cross-route state before sign-off.

The component contract is not the finished experience. A design system can provide good local behaviour, but product design still owns how that behaviour survives composition, state, routes and breakpoints.

This changes what is worth specifying. A static dialog frame is not enough. The design needs to show which control opens it, where focus returns, what changes behind it, what persists after navigation and what happens when the viewport collapses.

It also changes review. A visually convincing screen can compile and still fail the product. Review the keyboard path, the close path, the empty state, the mobile layout and the state transitions that cross component boundaries.

What this experiment cannot prove

This is evidence from one inexpensive model, not a universal ranking of AI coding methods. High-reasoning cells used three or four seeds per task. Arm B covered five of the eight tasks. Three tasks also received three runs with no extra reasoning. Reasoning mode removed temperature control, so variation was observed rather than suppressed.

Only 87 of 88 runs had browser evidence. Dependencies were prewarmed, so installation cost was not measured. The accessibility gap was driven mainly by one task. The study did not score visual quality, screen-reader experience, long-term maintenance or how the outputs respond to later design changes.

The strongest next replication is a frontier model. It may recreate the component API correctly and remove arm B’s build gap.

A neutral arm A′ should say: build the interface and extract reusable components where useful. That would show whether arm A’s page-specific code was the model’s preference or the prompt’s instruction.

A blocks-based arm C should copy larger interface sections that already connect primitives. The question is whether moving more connecting code into the registry reduces the 21.7% seam rate or simply moves the next seam elsewhere.

So, can copy-paste beat code generation?

Yes, when the component contract already exists.

Copying vetted source prevented the failures caused by recreating that source, reduced the amount of custom code the model had to author and carried useful palette and interaction decisions into the project. Generating directly also proved reliable: arm A completed 36 runs without a build failure.

The honest design rule is conditional. Copy stable components. Generate the product-specific work. Then test the seam between them as seriously as the component itself.