Streaming generative UI for React. Render HTML and schema-defined React components from LLM output.
TypeScript
3
7 commits
updated Sep 20, 2026
Streaming generative UI for React.
Playground · Inline chat · How It Works · Tailwind example · API · Contributing
Render HTML layouts and your schema-defined React components progressively from LLM output. Text appears as it arrives; charts gain complete data points and new series while the rest of the reply is still streaming. StreamTag UI updates your registered components without executing generated JavaScript.
Ask a question. Watch the reply become an interactive interface. Keep chatting. This recording shows a real model response with fictional business data: chart points and table rows arrive inside the message, a legend toggle works, and a follow-up uses the conversation context. No separate result page.
Watch the full recording · Run this example
defineComponent, getComponentDescriptions, and StreamRenderer.StreamTag UI uses an HTML/XML-style DSL: HTML tags define layouts, and custom tags describe registered React components and their props.
npm install streamtag-ui zod
Use React 18.2 or 19 in your application. See the API example below and the changelog.

A recording of the actual playground with fictional data. Watch the second line appear while the chart instance stays mounted. Watch the full recording.
Requirements: Node.js 22.12 or newer and pnpm 10.30.3. The repository pins its pnpm version through packageManager.
git clone https://github.com/xiaoosi/streamtag-ui.git
cd streamtag-ui
pnpm install
pnpm dev
Open the local URL printed by Vite. Press Play stream to replay a fictional report with an ECharts line chart and a React table. No API key, remote font, or model account is needed. Use one-character chunks to inspect partial numbers, pause or step the stream, edit the markup, and inspect generated model instructions or errors.
The playground is a deterministic replay tool. It does not connect to a model service or manage API keys.
The chat starter connects to a real model and renders every assistant reply directly in the conversation. Text, growing chart series, tables, and interactive checklists can appear in the same message. Continue the conversation to ask follow-up questions.
cp examples/chat/.env.example examples/chat/.env
# Set MODEL_BASE_URL, MODEL_API_KEY, and MODEL_NAME in that file.
pnpm dev:chat
Open http://127.0.0.1:5174. The starter uses the published npm package and a server-side OpenAI-compatible Chat Completions adapter. You can copy examples/chat into an independent project. See its setup, component guide, and interaction boundaries.
Your application owns the model, transport, and CSS. StreamTag turns the growing text into ordinary HTML and registered React components.
flowchart LR
A[React component + Zod schema] --> B[getComponentDescriptions]
B --> C[Your model]
C --> D[Accumulated markup]
D --> E[Incremental parser]
E --> F[Schema-valid props]
F --> G[Your React components]
E --> H[HTML layout]
defineComponent pairs a trusted React component with its Zod props schema. getComponentDescriptions turns that catalog into markup instructions and JSON Schemas for your model.StreamRenderer. Its parser consumes the appended suffix and keeps stable node identities. No generated JavaScript is evaluated.For example, <item>12 contributes no numeric point yet. After 0</item> arrives, the chart receives 120 once. It never receives the intermediate values 1 or 12. See streaming semantics for nested objects, fallbacks, and errors.
import { z } from 'zod';
import {
defineComponent,
getComponentDescriptions,
StreamRenderer,
} from 'streamtag-ui';
const scoreCard = defineComponent({
name: 'score-card',
description: 'Display a title and numeric score.',
schema: z.object({ title: z.string(), score: z.number() }),
component: ({ title, score }) => (
<article className="score-card">
<h2>{title}</h2>
<strong>{score}</strong>
</article>
),
fallback: <p>Waiting for the score…</p>,
});
// Keep definitions outside the render function so component identity stays stable.
const components = [scoreCard];
const instructions = getComponentDescriptions(components);
export function Report({
text,
generating,
}: {
text: string;
generating: boolean;
}) {
return (
<StreamRenderer
components={components}
content={text}
streaming={generating}
onError={(issue) => console.error(issue.code, issue.message)}
/>
);
}
Include instructions in the system prompt of your own model integration. Pass the accumulated output into content and set streaming={false} when generation finishes. The generated markup looks like this:
<section class="report">
<h1>A small win</h1>
<score-card>
<title>Weekly score</title>
<score>120.5</score>
</score-card>
</section>
A static saved result uses the same renderer with streaming omitted. The default is false.
The library only passes schema-valid props to your component. Before required props are available, it renders fallback (or nothing).
| Value | Publication rule |
|---|---|
| String | Grows while its tag is open, provided it passes its schema |
| Number, boolean, enum, literal | Published after the field closes |
| Object | Published once its current fields satisfy the schema |
| Array | Contains entries whose current values satisfy the item schema |
| Invalid closed field | Reports an error and shows the component fallback |
Nested object entries may appear before their closing tag once they are valid. This is what allows a series with a name and a default empty data array to render, then receive points incrementally. A table row with required numeric columns appears once those columns close. This version does not offer a separate "wait for the entire object to close" policy.
For a streaming chart, use defaults for arrays that can legitimately start empty:
const schema = z.object({
title: z.string(),
series: z
.array(
z.object({
name: z.string(),
data: z.array(z.number()).default([]),
}),
)
.default([]),
});
Arrays with .min(...) and strings with minimum lengths wait until their constraints are satisfied. No partial numeric prefix is interpreted as a completed number.
Select Team overview · Tailwind in the playground, or run the standalone example:
pnpm --filter streamtag-ui build
pnpm --filter @streamtag-ui/tailwind-react dev

The XML supplies layout classes; the registered React component uses Tailwind for its own internals. Both use the host's compiled stylesheet. The renderer has no Tailwind dependency.
<section class="grid gap-5 text-slate-900">
<h2 class="text-2xl font-semibold">Team overview</h2>
<activity-feed>
<title>Recent activity</title>
<items>
<item><id>deploy</id><title>Production deployment</title><detail>Shipped to all regions.</detail><status>Completed</status></item>
</items>
</activity-feed>
</section>
Tailwind must compile the classes before model output arrives. This example scans the XML fixture and component source with @source. For a live integration, give the model a known class vocabulary and include those classes in source files or @source inline(...). Arbitrary runtime class names do not automatically generate CSS. See the example and setup.
Use the streamtag-ui/server entry point and a shared metadata-only catalog to avoid importing React or browser-only chart code into your model-calling server:
import { z } from 'zod';
import { getComponentDescriptions } from 'streamtag-ui/server';
export const scoreDefinition = {
name: 'score-card',
description: 'Display a title and numeric score.',
schema: z.object({ title: z.string(), score: z.number() }),
};
const systemPrompt = getComponentDescriptions([scoreDefinition]);
On the client, call defineComponent({ ...scoreDefinition, component: ScoreCard }). Both entry points belong to the same npm package.
key.packages/streamtag-ui/ Published library; parser, runtime, and renderer are internal modules
apps/playground/ Replay playground and real ECharts/React examples
examples/minimal-react/ Small public-API-only consumer
examples/tailwind-react/ Tailwind CSS 4 layout and streaming React component
examples/chat/ Real-model inline chat starter using the npm package
fixtures/ Markup shared by demonstrations and regression tests
docs/ Syntax, behavior, integration, and architecture
pnpm check # Formatting, builds, type checks, and unit tests
pnpm exec playwright install chromium
pnpm test:browser # Chromium integration and responsive layout tests
pnpm --filter @streamtag-ui/minimal-react dev
pnpm --filter @streamtag-ui/tailwind-react dev
pnpm dev:chat # Real model; configure examples/chat/.env first
See CONTRIBUTING.md, quick start, streaming, and architecture.
MIT. See LICENSE.
7 commits
TypeScript
76.7%
CSS
12.6%
JavaScript
10.3%
Streaming generative UI for React. Render HTML and schema-defined React components from LLM output.
TypeScript
3
7 commits
updated Sep 20, 2026
Streaming generative UI for React.
Playground · Inline chat · How It Works · Tailwind example · API · Contributing
Render HTML layouts and your schema-defined React components progressively from LLM output. Text appears as it arrives; charts gain complete data points and new series while the rest of the reply is still streaming. StreamTag UI updates your registered components without executing generated JavaScript.
Ask a question. Watch the reply become an interactive interface. Keep chatting. This recording shows a real model response with fictional business data: chart points and table rows arrive inside the message, a legend toggle works, and a follow-up uses the conversation context. No separate result page.
Watch the full recording · Run this example
defineComponent, getComponentDescriptions, and StreamRenderer.StreamTag UI uses an HTML/XML-style DSL: HTML tags define layouts, and custom tags describe registered React components and their props.
npm install streamtag-ui zod
Use React 18.2 or 19 in your application. See the API example below and the changelog.

A recording of the actual playground with fictional data. Watch the second line appear while the chart instance stays mounted. Watch the full recording.
Requirements: Node.js 22.12 or newer and pnpm 10.30.3. The repository pins its pnpm version through packageManager.
git clone https://github.com/xiaoosi/streamtag-ui.git
cd streamtag-ui
pnpm install
pnpm dev
Open the local URL printed by Vite. Press Play stream to replay a fictional report with an ECharts line chart and a React table. No API key, remote font, or model account is needed. Use one-character chunks to inspect partial numbers, pause or step the stream, edit the markup, and inspect generated model instructions or errors.
The playground is a deterministic replay tool. It does not connect to a model service or manage API keys.
The chat starter connects to a real model and renders every assistant reply directly in the conversation. Text, growing chart series, tables, and interactive checklists can appear in the same message. Continue the conversation to ask follow-up questions.
cp examples/chat/.env.example examples/chat/.env
# Set MODEL_BASE_URL, MODEL_API_KEY, and MODEL_NAME in that file.
pnpm dev:chat
Open http://127.0.0.1:5174. The starter uses the published npm package and a server-side OpenAI-compatible Chat Completions adapter. You can copy examples/chat into an independent project. See its setup, component guide, and interaction boundaries.
Your application owns the model, transport, and CSS. StreamTag turns the growing text into ordinary HTML and registered React components.
flowchart LR
A[React component + Zod schema] --> B[getComponentDescriptions]
B --> C[Your model]
C --> D[Accumulated markup]
D --> E[Incremental parser]
E --> F[Schema-valid props]
F --> G[Your React components]
E --> H[HTML layout]
defineComponent pairs a trusted React component with its Zod props schema. getComponentDescriptions turns that catalog into markup instructions and JSON Schemas for your model.StreamRenderer. Its parser consumes the appended suffix and keeps stable node identities. No generated JavaScript is evaluated.For example, <item>12 contributes no numeric point yet. After 0</item> arrives, the chart receives 120 once. It never receives the intermediate values 1 or 12. See streaming semantics for nested objects, fallbacks, and errors.
import { z } from 'zod';
import {
defineComponent,
getComponentDescriptions,
StreamRenderer,
} from 'streamtag-ui';
const scoreCard = defineComponent({
name: 'score-card',
description: 'Display a title and numeric score.',
schema: z.object({ title: z.string(), score: z.number() }),
component: ({ title, score }) => (
<article className="score-card">
<h2>{title}</h2>
<strong>{score}</strong>
</article>
),
fallback: <p>Waiting for the score…</p>,
});
// Keep definitions outside the render function so component identity stays stable.
const components = [scoreCard];
const instructions = getComponentDescriptions(components);
export function Report({
text,
generating,
}: {
text: string;
generating: boolean;
}) {
return (
<StreamRenderer
components={components}
content={text}
streaming={generating}
onError={(issue) => console.error(issue.code, issue.message)}
/>
);
}
Include instructions in the system prompt of your own model integration. Pass the accumulated output into content and set streaming={false} when generation finishes. The generated markup looks like this:
<section class="report">
<h1>A small win</h1>
<score-card>
<title>Weekly score</title>
<score>120.5</score>
</score-card>
</section>
A static saved result uses the same renderer with streaming omitted. The default is false.
The library only passes schema-valid props to your component. Before required props are available, it renders fallback (or nothing).
| Value | Publication rule |
|---|---|
| String | Grows while its tag is open, provided it passes its schema |
| Number, boolean, enum, literal | Published after the field closes |
| Object | Published once its current fields satisfy the schema |
| Array | Contains entries whose current values satisfy the item schema |
| Invalid closed field | Reports an error and shows the component fallback |
Nested object entries may appear before their closing tag once they are valid. This is what allows a series with a name and a default empty data array to render, then receive points incrementally. A table row with required numeric columns appears once those columns close. This version does not offer a separate "wait for the entire object to close" policy.
For a streaming chart, use defaults for arrays that can legitimately start empty:
const schema = z.object({
title: z.string(),
series: z
.array(
z.object({
name: z.string(),
data: z.array(z.number()).default([]),
}),
)
.default([]),
});
Arrays with .min(...) and strings with minimum lengths wait until their constraints are satisfied. No partial numeric prefix is interpreted as a completed number.
Select Team overview · Tailwind in the playground, or run the standalone example:
pnpm --filter streamtag-ui build
pnpm --filter @streamtag-ui/tailwind-react dev

The XML supplies layout classes; the registered React component uses Tailwind for its own internals. Both use the host's compiled stylesheet. The renderer has no Tailwind dependency.
<section class="grid gap-5 text-slate-900">
<h2 class="text-2xl font-semibold">Team overview</h2>
<activity-feed>
<title>Recent activity</title>
<items>
<item><id>deploy</id><title>Production deployment</title><detail>Shipped to all regions.</detail><status>Completed</status></item>
</items>
</activity-feed>
</section>
Tailwind must compile the classes before model output arrives. This example scans the XML fixture and component source with @source. For a live integration, give the model a known class vocabulary and include those classes in source files or @source inline(...). Arbitrary runtime class names do not automatically generate CSS. See the example and setup.
Use the streamtag-ui/server entry point and a shared metadata-only catalog to avoid importing React or browser-only chart code into your model-calling server:
import { z } from 'zod';
import { getComponentDescriptions } from 'streamtag-ui/server';
export const scoreDefinition = {
name: 'score-card',
description: 'Display a title and numeric score.',
schema: z.object({ title: z.string(), score: z.number() }),
};
const systemPrompt = getComponentDescriptions([scoreDefinition]);
On the client, call defineComponent({ ...scoreDefinition, component: ScoreCard }). Both entry points belong to the same npm package.
key.packages/streamtag-ui/ Published library; parser, runtime, and renderer are internal modules
apps/playground/ Replay playground and real ECharts/React examples
examples/minimal-react/ Small public-API-only consumer
examples/tailwind-react/ Tailwind CSS 4 layout and streaming React component
examples/chat/ Real-model inline chat starter using the npm package
fixtures/ Markup shared by demonstrations and regression tests
docs/ Syntax, behavior, integration, and architecture
pnpm check # Formatting, builds, type checks, and unit tests
pnpm exec playwright install chromium
pnpm test:browser # Chromium integration and responsive layout tests
pnpm --filter @streamtag-ui/minimal-react dev
pnpm --filter @streamtag-ui/tailwind-react dev
pnpm dev:chat # Real model; configure examples/chat/.env first
See CONTRIBUTING.md, quick start, streaming, and architecture.
MIT. See LICENSE.
7 commits
TypeScript
76.7%
CSS
12.6%
JavaScript
10.3%