Skip to content

Generative UI

How to let an AI model build screens with dsui components. OpenUI and json-render are supported.

What generative UI means

Generative UI is when an AI model answers with a screen instead of text. The model writes a screen using only a list of components you set in advance, and your app draws that answer with the real components. With dsui, a screen a model made has the same tokens, motion and sound as one a person made.

You define a component once and use it in both frameworks.

FrameworkWhat the model writesWhat dsui produces
OpenUIOpenUI Lang (a short function-call syntax)A library built with defineComponent and createLibrary, and the system prompt
json-renderA JSON specA catalog and registry built with defineCatalog and defineRegistry, and the system prompt

Define a component once

The neutral spec holds the name, when a model should pick it (description), the props (zod), whether it takes children (slot) and the events it can fire (events). Props are serializable only: strings, numbers, booleans and enums. A model cannot write a callback or a node, so behaviour reaches your app through emit (events) and setValue (input values).

Source codetsx
import { z } from "zod";
import { defineGenUi } from "@/components/arc/gen-ui/spec";
import { Button } from "@/components/arc/button/button";

export const GenButton = defineGenUi({
  name: "Button",
  description: "A button that triggers an action. Use a verb phrase as the label.",
  props: z.object({
    label: z.string(),
    variant: z.enum(["primary", "secondary", "ghost"]).optional(),
  }),
  events: ["press"],
  render: ({ props, emit }) => (
    <Button variant={props.variant} onClick={() => emit("press")}>
      {props.label}
    </Button>
  ),
});

OpenUI Lang passes arguments by position, so list required props first and optional props after them. A component that takes children puts the child list first (Card([...], "Title")).

Use it in OpenUI

Turn the spec into a library and you get both the system prompt for the model and the Renderer that draws the result. An event becomes an action prop (on<Event> when there are several), and input values collect in OpenUI's form state. A component that takes children receives a children array.

Source codetsx
// Server (a route handler): the system prompt for the model.
import { GENUI_RULES, GENUI_SPEC } from "@/components/arc/gen-ui/components";
import { openUiPrompt } from "@/components/arc/gen-ui/openui";

const system = openUiPrompt(GENUI_SPEC, { rules: GENUI_RULES });

// Client: draw what the model wrote.
import { GenUi } from "@/components/arc/gen-ui/gen-ui";

<GenUi framework="openui" response={text} />;

Use it in json-render

The same spec yields a catalog and a registry. An optional prop is accepted as null from the model and reaches the dsui component as an omitted prop. Children go in the default slot, and input values collect in state at /form/<name>.

Source codetsx
// Server (a route handler): the system prompt for the model.
import { GENUI_RULES, GENUI_SPEC } from "@/components/arc/gen-ui/components";
import { jsonRenderPrompt } from "@/components/arc/gen-ui/json-render";

const system = jsonRenderPrompt(GENUI_SPEC, { rules: GENUI_RULES });

// Client: draw the spec the model wrote.
import { GenUi } from "@/components/arc/gen-ui/gen-ui";

<GenUi framework="json-render" response={spec} />;

Install it, and what works now and what does not

gen-ui installs with one line like any other component. The components it uses (Button, Input, Textarea, Select, Switch, Checkbox, Alert, Badge, MetricCard, Progress, Stack and Card) and the npm packages it needs (@openuidev/react-lang, @json-render/react and zod) come along.

Terminalbash
npx shadcn@latest add https://dsui.vercel.app/r/gen-ui.json

This is a preview, so it has twelve components. The input fields (Input, Textarea, Select, Switch and Checkbox) report their value to your app under their name.

  • A component that takes children holds only the components listed before it in the spec. So a Card can hold a Stack (a horizontal row), but there is no Stack inside a Stack and no Card inside a Card.
  • The dsui rules, such as the polite Korean tone and button wording, go into both prompts as the same sentences through GENUI_RULES.
  • More components follow the same way. A new component needs only its spec, and both adapters pick it up.