View all articles

Published / 5 min

Generative UI with AI: useful interfaces, not improvised HTML

Turn model responses into safe React components with structured data, explicit states, boundaries, and a practical example.

Someone asks "How did my sales change?" and receives five paragraphs describing a chart they cannot see. Generative UI tries to remove that friction: depending on the intent, the app shows an appropriate component —a chart, a summary, a card— instead of text alone. The point is not to have a model write arbitrary HTML, but to let your application control which pieces it can display.

From response to interface

In a traditional chat the output is text. In generative UI, a server can fetch data using a tool and return a structured result that the frontend maps to known components. For example:

  1. A person requests a September sales summary.
  2. The model chooses an authorized get_sales tool.
  3. The server fetches data, checks permissions, and returns a result with a known schema.
  4. React chooses MetricCard or SummaryCard; the model is not allowed to inject arbitrary JSX.

That is also the approach in Vercel AI SDK's generative UI guide: tools produce data and the interface renders the corresponding components. You can apply the same pattern without that library if your product uses a different stack.

When is it actually useful?

It makes sense when the right format depends on the question: a weather forecast calls for a weather card; an order needs a status and a link; a comparison needs a table. A settings page with a fixed structure does not need AI to decide its layout. Nor does generative UI replace good design: dynamically producing unreadable cards only multiplies the problem.

Tip: first design three excellent responses without AI. Then let a model choose among them through structured data. Do not start with "the model can draw anything."

A simple contract between backend and React

Suppose an API, after querying authorized data, returns one of two shapes. The example does not call a model: it shows the safe boundary that any model or tool must respect before rendering:

type Result =
  | { type: "metric"; label: string; value: number; unit: string }
  | { type: "summary"; text: string };

function parseResult(value: unknown): Result | null {
  if (!value || typeof value !== "object") return null;
  const item = value as Record<string, unknown>;

  if (item.type === "metric" && typeof item.label === "string" &&
      typeof item.value === "number" && Number.isFinite(item.value) &&
      typeof item.unit === "string") {
    return { type: "metric", label: item.label, value: item.value, unit: item.unit };
  }
  if (item.type === "summary" && typeof item.text === "string") {
    return { type: "summary", text: item.text };
  }
  return null;
}

Repeat validation on the server with a real schema or validator before sending data; the client check also protects the rendering boundary. In production, limit text lengths, allowed units, and out-of-range values.

function GeneratedResult({ value }: { value: unknown }) {
  const result = parseResult(value);
  if (!result) return <p>This response could not be displayed.</p>;

  switch (result.type) {
    case "metric":
      return <MetricCard label={result.label} value={result.value} unit={result.unit} />;
    case "summary":
      return <p>{result.text}</p>;
  }
}

MetricCard is your component: designed, tested, and accessible. React escapes text by default; avoid turning model output into HTML with dangerouslySetInnerHTML. A response schema is easier to evolve than an improvised interface for every prompt.

Streaming does not remove UI states

When data arrives in chunks, show loading, partial results only when valid, recoverable errors, and the final result. Do not draw an incomplete object as if it were definitive. Reserve space for the card to prevent layout shifts; if the tool takes time, show its state rather than an empty card. When a model fails, the app needs a comprehensible fallback and a way to retry.

For sensitive products, add clear limits: the model suggests, your code validates, and the person confirms actions that change money, data, or permissions. A wrong chart misinforms; an unchecked generated "confirm payment" button can do real harm.

Tips and common mistakes

  • Separate content from presentation. Have the server return data and a type, not an HTML string.
  • Instrument the flow. Measure tool failures, which variant is displayed, and whether people understand the result; do not measure only tokens.
  • Design accessibility from the start. Headings, labels, table alternatives to charts, and visible focus also apply to generated content.
  • Avoid unlimited components. A small registry of variants is easier to test than hundreds of prompt-selected widgets.
  • Never treat a model's instruction as authorization. Your API checks access before returning data.

Take it back to your project

Start with one question that you currently answer with too much text. Design a card that answers it better; define verifiable data, handle four states (loading, ready, empty, error), and then connect a tool or model. Generative UI does not mean giving up design: it means showing good design at the right moment.

Source: Vercel AI SDK: Generative User Interfaces.