← All essays

Your Designer Learned to Vibe-Code? Give Him a Playground

Thirty-nine widgets and a design system, written by someone who is not an engineer, inside a boundary that makes it safe

The browser-side UI of our virtual lab — thermometers, scales, stopwatches, force sensors, graduated cylinders, a microscope, a tuning fork, a smart cart track, about thirty-nine instrument widgets in total, plus the design tokens they all draw from — was written by our designer, largely with an AI assistant, and largely without him being an engineer.

It ships. It is in production. I am not nervous about it.

That last sentence is the whole article, and the reason it is true has nothing to do with how good the code is. It is true because of where the code is allowed to live.

What "vibe-coding" is actually good at

Set aside whether the term is flattering. As a way of working it means: iterate quickly against visible feedback, steered by taste rather than by a complete model of the system underneath.

For interface work that is not a compromise, it is close to ideal. A thermometer widget is a design problem. The questions that matter are whether the mercury column reads at a glance, whether the tick labels survive at small sizes, whether the needle motion feels like an instrument rather than an animation. Those are answered by looking, adjusting, and looking again — which is exactly the loop this way of working optimises for. Somebody with the taste to answer them and a tight enough feedback cycle will beat an engineer reasoning about it from first principles.

The failure mode is equally clear. It goes wrong when a change can reach further than the person can see, and when the feedback loop is slower than the iteration rate.

So the job is not to teach the designer the whole system. It is to build a place where neither of those failure conditions holds.

Wall one: the engine is sealed

The runtime has a core — lifecycle dispatch, configuration, the shared-memory bridge to the 3D runtime, the math primitives. It is marked sealed, and the policy is one sentence in the project's instructions to everyone including the AI assistant:

Engine is SEALED. Do not edit it. Add features as handlers.

Not a suggestion. A sealed directory with a stated rule, repeated in the contributor docs, the package spec, and the README.

The kit — the designer's territory — is a separate top-level package with a hard boundary: no import anywhere inside it may reach outside the package. Not into the engine, not into the wrapper, not into the Unity project. The kit is self-contained by construction.

Where the kit genuinely needs engine code — vector and quaternion math, the link channel — it gets a byte-identical copy under a directory whose name starts with an underscore, and the spec explains what the underscore means: shimmed engine code, not part of the kit's authored surface — do not extend or refactor as if it were kit-original.

That is a small, legible convention doing real work. It marks the difference between "code you own" and "code you are borrowing", in a form you can see in an import path.

Wall two: the contract is four methods

The entire extension API is this:

class MyHandler {
    onStepChanged(step, currentState) {}   // once per step change
    onFrame(scene, output, step, dt) {}    // every frame
    onObjectSpawned(data) {}               // a new object appeared
    onObjectDestroyed(data) {}             // an object went away
}

All optional, duck-typed. Register it and the engine calls whichever ones exist.

You can explain that in a sentence, and I have — repeatedly, to people who are not engineers, successfully. There is no lifecycle diagram, no base class, no decorators, no container, no plugin manifest.

That size is not an accident and it is not laziness. A small contract is what makes the difference between someone who understands the system and someone with taste and an AI assistant much smaller than it would otherwise be. Most of the complexity you would normally have to internalise before contributing simply is not on the surface.

Wall three: widgets cannot reach the simulation

The strongest boundary is the one that is structural rather than stated.

Widgets are plain custom elements. They receive state through attributes and properties, and they communicate by dispatching DOM events. The spec says it directly: widgets do not call any wrapper or engine API.

Which means a widget is incapable of touching physics. Not discouraged from it — incapable. It has no reference to the scene, no reference to the override buffer, no way to move an object or advance a step. It receives a number and renders it. Somebody who has never heard of the shared-memory bridge can write a beautiful gauge, and the worst outcome is a gauge that looks wrong.

Thirty-nine widgets, and the blast radius of every one of them is its own bounding box.

Handlers are the wider surface — they do receive the scene and the output buffer — and they are correspondingly fewer, more reviewed, and more often written jointly. The split is deliberate: the surface with real power is small and the surface with real volume is powerless.

The feedback loop is the actual product

Boundaries only solve half of it. The other half is seeing your change immediately, and this is where most of the investment went.

The kit ships its own catalog: a set of standalone HTML pages, one per widget or authoring pattern, plus a design-system reference and a camera-preset reference. Open a file, see the component, edit, reload. No Unity build, no backend, no lab document, no waiting.

Alongside it sits a set of demo scenes — one small authored document per interaction type, which double as smoke tests. Want to know whether your change to the drag handler broke click-to-place? There is a scene for click-to-place.

This is the part I would tell people to build first, ahead of any boundary work. A person iterating on taste needs the loop to be fast, and a slow loop does not just make them slower — it changes what they attempt. Nobody explores a fourth variant of a gauge when each variant costs a four-minute build.

The catalog is also what makes review tractable from my side. I do not have to run the whole application to see what changed; I open the page.

The copy is disposable

One more structural piece, less obvious but load-bearing.

The kit is a source-of-truth package at the top of the repository. The application consumes a vendored copy of it — regenerated on every boot, git-ignored, synchronised with rsync --delete.

So there are two directories with the same contents, and exactly one of them is real. Editing the copy is not forbidden by politeness; it is futile, because the next boot wipes it. Each mirror root even carries a marker file restating this.

The value here is that "where do I make this change" has one answer, and getting it wrong is self-correcting within minutes rather than producing a mysterious divergence three weeks later. For someone still building a mental model of the repository, a mistake that undoes itself is enormously better than one that persists quietly.

The wall that is not there

I said I am not nervous. That is not the same as saying nothing can go wrong, and there is exactly one place where a kit change reaches the whole application.

Handlers run inside the 3D runtime's frame. Synchronously. Not scheduled for later — the runtime calls into JavaScript in the middle of its update and blocks until it returns. Rendering and physics take ten to twelve milliseconds of the sixteen available at sixty frames per second; the rest belongs to the handlers.

A handler that allocates in a loop, or creates DOM elements per frame, or does layout work in the middle of that window, does not produce a subtly wrong widget. It stalls the entire application, and the symptom shows up as the 3D scene stuttering, which looks like a rendering problem rather than a UI one.

No boundary prevents this. It is not a permission the kit could be denied — the frame callback is the whole point of a handler. The mitigations are the ordinary ones: the budget is documented in the contributor instructions with the actual numbers, the rules are concrete rather than abstract ("create elements once and reposition them; never create and destroy per frame"), and performance is the thing I check first in review.

If I were starting again I would add a development-mode timer around each handler's frame call that logs when one exceeds a millisecond. That converts a rule you have to remember into a message that arrives when you break it — which is the same trick as the disposable copy, applied to the one wall I could not build.

What I would tell a team lead

If someone on your team has started producing real code with an AI assistant and you are trying to decide how to feel about it, my suggestion is to stop evaluating the person and go look at your architecture.

The question is not is this person good enough to commit to our codebase. It is does our codebase have a place where a change cannot reach further than the person can see. If it does not, that is a fact about your system, and it was already true for your engineers — it just had not been tested as visibly.

Build the place. It needs four things:

Then let people work. The output will vary in quality, the way all output does, and you will review it the way you review everything else. What you will not be doing is losing sleep over whether a thermometer took down the physics.

Thirty-nine widgets, a design system, and a catalog to preview them in. The credit for the widgets belongs to the designer. The credit for me not worrying belongs to the boundary — and the boundary was cheaper to build than any single one of the widgets.