xiaoosi/streamtag-ui

Streaming generative UI for React. Render HTML and schema-defined React components from LLM output.

TypeScript

3

7 commits

updated Sep 20, 2026

See the code
generative-ui
llm
llm-ui
react
react-components
streaming
streaming-ui
typescript
zod

See what people are saying (1)

SourceMessageScoreDate

I built StreamTag UI: stream schema-validated React components from LLM output (r/reactjs)

Partial JSON is awkward for generative UI. I kept running into the same problem: how do you let an LLM progressively update real React components without generating executable code or waiting for one giant structured response to finish? I built StreamTag UI around an HTML/XML-style stream. Normal…

0

Sep 21, 2026

README

StreamTag UI

Streaming generative UI for React.

License: MIT TypeScript React Zod

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.

Live inline chat: a natural-language request becomes a growing two-series chart and profit table, followed by an answer in the same conversation.

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

  • Three APIs: defineComponent, getComponentDescriptions, and StreamRenderer.
  • Your components: use an existing chart, table, card, or product component.
  • Your CSS: ordinary stylesheets, utility classes, and inline styles work within the supported HTML subset. CSS is supplied by the host application.
  • Your model and transport: pass accumulated text from any source.
  • One package: TypeScript types, a browser renderer, and a React-free server description entry point.

StreamTag UI uses an HTML/XML-style DSL: HTML tags define layouts, and custom tags describe registered React components and their props.

Install

npm install streamtag-ui zod

Use React 18.2 or 19 in your application. See the API example below and the changelog.

See it stream

Recorded playground demo: markup arrives, chart series grow point by point, and validated rows populate a React table.

A recording of the actual playground with fictional data. Watch the second line appear while the chart instance stays mounted. Watch the full recording.

Try the playground

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.

Try inline chat

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.

How It Works

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]
  1. Define and describe. defineComponent pairs a trusted React component with its Zod props schema. getComponentDescriptions turns that catalog into markup instructions and JSON Schemas for your model.
  2. Parse only new text. Pass the full response received so far to StreamRenderer. Its parser consumes the appended suffix and keeps stable node identities. No generated JavaScript is evaluated.
  3. Publish valid values. Text can grow while its tag is open. Numbers, booleans, enums, and literals wait for a closing tag. Objects and array entries become available when their current values satisfy the schema.
  4. Update the existing UI. Changed props flow into the mounted component. A series can start with an empty array, gain complete points, and be joined by a second series before the entire chart closes.

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.

The three APIs

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.

Streaming behavior

The library only passes schema-valid props to your component. Before required props are available, it renders fallback (or nothing).

ValuePublication rule
StringGrows while its tag is open, provided it passes its schema
Number, boolean, enum, literalPublished after the field closes
ObjectPublished once its current fields satisfy the schema
ArrayContains entries whose current values satisfy the item schema
Invalid closed fieldReports 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.

Tailwind CSS

Select Team overview · Tailwind in the playground, or run the standalone example:

pnpm --filter streamtag-ui build
pnpm --filter @streamtag-ui/tailwind-react dev

Tailwind example in the playground: responsive utility-class cards and a streaming React activity feed.

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.

Server-side model instructions

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.

Scope and boundaries

  • Supports React 18.2/19 and Zod 4. Build output is ESM with TypeScript declarations.
  • Supported schemas: objects, arrays, strings, finite numbers, booleans, enums, scalar literals, optional fields, and defaults. See syntax for exact boundaries.
  • HTML uses a documented allowlist. Script, iframe, style, event-handler, and executable URL output are not supported. Model output is data, not executable component code.
  • The renderer is not a browser security sandbox. Trusted registered components control their own behavior. Styles can affect layout, and supported image/link URLs can reference external resources. Use appropriate host isolation when handling untrusted sources.
  • CSS classes must exist in the host. Tailwind runtime generation and CSS Modules class-name mapping are application concerns.
  • No model SDK, chat state, transport, arbitrary code execution, or built-in chart library is included.
  • Appended text is parsed incrementally. Replaced or shortened content starts a new document. To explicitly start a new stream whose text happens to share the previous prefix, change the renderer's React key.

Repository

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

Development

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.

License

MIT. See LICENSE.

Contributors

xiaoosi

7 commits

xiaoosi/streamtag-ui

Streaming generative UI for React. Render HTML and schema-defined React components from LLM output.

TypeScript

3

7 commits

updated Sep 20, 2026

See the code
generative-ui
llm
llm-ui
react
react-components
streaming
streaming-ui
typescript
zod

See what people are saying (1)

SourceMessageScoreDate

I built StreamTag UI: stream schema-validated React components from LLM output (r/reactjs)

Partial JSON is awkward for generative UI. I kept running into the same problem: how do you let an LLM progressively update real React components without generating executable code or waiting for one giant structured response to finish? I built StreamTag UI around an HTML/XML-style stream. Normal…

0

Sep 21, 2026

README

StreamTag UI

Streaming generative UI for React.

License: MIT TypeScript React Zod

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.

Live inline chat: a natural-language request becomes a growing two-series chart and profit table, followed by an answer in the same conversation.

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

  • Three APIs: defineComponent, getComponentDescriptions, and StreamRenderer.
  • Your components: use an existing chart, table, card, or product component.
  • Your CSS: ordinary stylesheets, utility classes, and inline styles work within the supported HTML subset. CSS is supplied by the host application.
  • Your model and transport: pass accumulated text from any source.
  • One package: TypeScript types, a browser renderer, and a React-free server description entry point.

StreamTag UI uses an HTML/XML-style DSL: HTML tags define layouts, and custom tags describe registered React components and their props.

Install

npm install streamtag-ui zod

Use React 18.2 or 19 in your application. See the API example below and the changelog.

See it stream

Recorded playground demo: markup arrives, chart series grow point by point, and validated rows populate a React table.

A recording of the actual playground with fictional data. Watch the second line appear while the chart instance stays mounted. Watch the full recording.

Try the playground

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.

Try inline chat

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.

How It Works

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]
  1. Define and describe. defineComponent pairs a trusted React component with its Zod props schema. getComponentDescriptions turns that catalog into markup instructions and JSON Schemas for your model.
  2. Parse only new text. Pass the full response received so far to StreamRenderer. Its parser consumes the appended suffix and keeps stable node identities. No generated JavaScript is evaluated.
  3. Publish valid values. Text can grow while its tag is open. Numbers, booleans, enums, and literals wait for a closing tag. Objects and array entries become available when their current values satisfy the schema.
  4. Update the existing UI. Changed props flow into the mounted component. A series can start with an empty array, gain complete points, and be joined by a second series before the entire chart closes.

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.

The three APIs

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.

Streaming behavior

The library only passes schema-valid props to your component. Before required props are available, it renders fallback (or nothing).

ValuePublication rule
StringGrows while its tag is open, provided it passes its schema
Number, boolean, enum, literalPublished after the field closes
ObjectPublished once its current fields satisfy the schema
ArrayContains entries whose current values satisfy the item schema
Invalid closed fieldReports 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.

Tailwind CSS

Select Team overview · Tailwind in the playground, or run the standalone example:

pnpm --filter streamtag-ui build
pnpm --filter @streamtag-ui/tailwind-react dev

Tailwind example in the playground: responsive utility-class cards and a streaming React activity feed.

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.

Server-side model instructions

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.

Scope and boundaries

  • Supports React 18.2/19 and Zod 4. Build output is ESM with TypeScript declarations.
  • Supported schemas: objects, arrays, strings, finite numbers, booleans, enums, scalar literals, optional fields, and defaults. See syntax for exact boundaries.
  • HTML uses a documented allowlist. Script, iframe, style, event-handler, and executable URL output are not supported. Model output is data, not executable component code.
  • The renderer is not a browser security sandbox. Trusted registered components control their own behavior. Styles can affect layout, and supported image/link URLs can reference external resources. Use appropriate host isolation when handling untrusted sources.
  • CSS classes must exist in the host. Tailwind runtime generation and CSS Modules class-name mapping are application concerns.
  • No model SDK, chat state, transport, arbitrary code execution, or built-in chart library is included.
  • Appended text is parsed incrementally. Replaced or shortened content starts a new document. To explicitly start a new stream whose text happens to share the previous prefix, change the renderer's React key.

Repository

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

Development

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.

License

MIT. See LICENSE.

Contributors

xiaoosi

7 commits

Languages

TypeScript

76.7%

CSS

12.6%

JavaScript

10.3%