DEV Community

Cover image for The Missing Layer Between LLMs and Your DOM
Helmuth Saatkamp
Helmuth Saatkamp

Posted on

The Missing Layer Between LLMs and Your DOM

Every serious AI product is racing toward the same feature: instead of returning text, return UI. Ask for a dashboard, get a dashboard. Ask for a form, get a working form. Not a JSON blob you have to render yourself — the model produces the interface directly. This is a generative UI, and it's moving from demo trick to expected feature faster than most teams' infrastructure can handle it.

Nobody's talking enough about what breaks when you actually ship this.

Three problems everyone hits, in order

1. The output is untrusted, and you're about to render it anyway.

An LLM produces HTML/JS and you put it in the DOM. That's dangerouslySetInnerHTML with a model in the loop instead of a CMS. The model can hallucinate a <script> tag, an inline event handler, a fetch to somewhere it shouldn't. Most teams handle this by... not handling it, and hoping the system prompt is a security boundary. It isn't.

2. The model doesn't actually know your components.

Ask an LLM to generate a button using your design system, and it'll invent props that don't exist, skip required accessibility attributes, or confidently use an API from six versions ago because that's what's in its training data. There's no source of truth it's checking against at generation time — it's pattern-matching, not looking anything up.

3. Streaming UI is a different problem than streaming text.

Token-by-token text just appends. Token-by-token HTML doesn't — a half-closed tag, a component that needs its full prop set before it can render, a re-render that resets scroll position and loses focus every single chunk. Most "streaming UI" demos actually just wait for the full response and fake the typing effect, because doing it for real is harder.

Put together: generative UI needs an isolation boundary, a ground-truth check, and a real streaming render path. Most current implementations have zero of the three.

What an actual solution looks like

Isolation. Model output should render somewhere it structurally cannot reach the host page — no shared DOM, no cookie access, no ambient fetch. A sandboxed iframe with an auto-generated, restrictive Content-Security-Policy and a typed message channel back to the host, so errors and events cross the boundary deliberately instead of by accident.

Ground truth at generation time. The model shouldn't be guessing at your component API from memory. It should be able to query the real one — current props, current types, current examples — and, ideally, have its output checked against that same source before anything renders. Not a hand-maintained doc that drifts from the actual code; the actual code.

A real patch, not a re-render. Streaming HTML into a live preview needs to update in place — DOM diffed or body-replaced without tearing down scripts, state, or focus — with a resize/error channel back to the host so a bad chunk shows up as a message, not a crash.

A working example of this, end to end

This is the concrete version of the above, not a hypothetical, from an open-source TypeScript toolkit called Vielzeug:

  • An MCP server exposes a component library's real docs, type signatures, and examples so an AI client queries current truth instead of pattern-matching stale training data.
  • The component library itself ships a validation tool: generated markup gets checked against the same Custom Elements Manifest generated from the real build output — not a handwritten spec — before it's trusted.
  • A sandboxed iframe runtime handles the render: strict auto-generated CSP, allow-scripts only, a typed postMessage bridge, and a patch() call built specifically for streaming:
// Stream tokens from an LLM straight into an isolated, live preview
let accumulated = '';
for await (const chunk of streamUI(userPrompt)) {
  accumulated += chunk;
  preview.patch(accumulated); // live update, no reload, no lost state
}

// Errors from inside the sandbox surface as messages, not crashes
sandbox.onMessage((msg) => {
  if (msg.type === 'error') showError(msg.message);
});
Enter fullscreen mode Exit fullscreen mode

Query the real spec → generate against it → validate the result → render it somewhere that can't hurt you, live, as it streams. Three separate concerns, three separate pieces, wired together instead of duct-taped.

This doesn't replace your frontend — it plugs into it

Worth being direct about this, because the hype cycle isn't: generative UI is not coming for your design system, your hand-built dashboards, or your carefully tuned checkout flow. Nobody wants their core navigation, their onboarding, or their billing page re-rolled by a model on every visit. Static and dynamic UI — built, reviewed, tested, versioned by humans — stay exactly where they are for anything that needs to be consistent, accessible, and predictable every single time.

What generative UI is good for is the surface area that's always been expensive to hand-build for every possible case: an ad-hoc chart for whatever question a user just asked, a one-off form shaped by a conversation, a report nobody wrote a template for because there are infinite variations of it. That's a complement to your app, not a replacement of it.

The architecture reflects that split literally. In the example above, the sandbox is a slot inside an otherwise normal, hand-built page — an isolated container the rest of your static UI hosts, not a takeover of it. The design system is the same one your human-built screens already use; the model is just another producer of markup against it, held to the same spec, rendered in a contained space next to everything else you built the regular way. Generative UI earns a place inside the app you already have. It doesn't ask you to throw that app away.

The actual takeaway

None of this requires a specialized "AI framework." It requires the same things good frontend infrastructure has always required — typed contracts, real isolation, predictable state — pointed at a new kind of untrusted input, and applied only where that input actually earns its place. The teams that get generative UI right won't be the ones who replace their frontend with a model. They'll be the ones who gave the model a small, safe, well-defined slot to be useful in — and kept building everything else the way they already know it works.

Top comments (0)