A streaming-optimized HTML renderer for AI responses — the HTML counterpart to Streamdown.
AI models excel at writing HTML. It's more expressive than markdown: metric dashboards, comparison grids, styled diffs, rich tables, badges, and custom layouts — all in one stream. StreamHtml renders that HTML safely while it streams, handling incomplete tags gracefully.
npm install @alphanimble/streamhtml
Screen recording of the included chat demo (npm run chat) streaming a live OpenRouter response as HTML.
GitHub READMEs do not render <iframe> or <video> embeds (sanitized HTML only). Use the thumbnail below — click to watch on Streamable:
Direct link: youtube
What you're seeing in the recording:
rehtml() without React for Node or other frameworks<pre>, <code>, etc. where < is literalRequires React 18+ (and react-dom in browser apps). Import the base styles once:
import "@alphanimble/streamhtml/styles.css";
import { StreamHtml } from "@alphanimble/streamhtml";
import "@alphanimble/streamhtml/styles.css";
function Message({ content, isStreaming }: { content: string; isStreaming: boolean }) {
return (
<StreamHtml isStreaming={isStreaming}>
{content}
</StreamHtml>
);
}
<StreamHtml
isStreaming={isStreaming}
reasoning={reasoningText}
thinkingLabel="Thinking"
>
{htmlContent}
</StreamHtml>
import { useChat } from "@ai-sdk/react";
import { StreamHtml } from "@alphanimble/streamhtml";
import "@alphanimble/streamhtml/styles.css";
export function Chat() {
const { messages, status } = useChat();
return (
{messages.map((message) => (
{message.parts.map((part, i) =>
part.type === "text" ? (
<StreamHtml
key={i}
isStreaming={
status === "streaming" &&
message.id === messages.at(-1)?.id
}
>
{part.text}
</StreamHtml>
) : null,
)}
))}
);
}
StreamHtml runs a repair pipeline on every chunk:
bold→bold3. **Split blocks** — Completed top-level blocks are frozen; only the tail re-renders
4. **Patch incrementally** — Plain text and table rows append to the live DOM
5. **Sanitize** — DOMPurify removes XSS vectors beforedangerouslySetInnerHTML`
Streaming input Repair Render
───────────────── ────────── ──────────
A
B stable: [
A
] memoized ✓
C live:
C
re-renders each chunk
<StreamHtml />| Prop | Type | Default | Description |
|---|---|---|---|
children |
string |
"" |
HTML content to render |
reasoning |
string |
"" |
Plain-text reasoning / thinking tokens |
isStreaming |
boolean |
false |
Shows caret, marks streaming state |
caret |
boolean |
isStreaming |
Toggle streaming caret |
repair |
boolean |
true |
Run incomplete HTML repair |
sanitize |
boolean |
true |
DOMPurify sanitization |
memoizeBlocks |
boolean |
true |
Freeze completed blocks |
sanitizeConfig |
Config |
— | DOMPurify config override |
thinkingLabel |
string |
"Thinking" |
Label for reasoning panel |
className |
string |
— | Root element class |
rehtml(input, options?)Headless repair function for non-React use:
import { rehtml } from "@alphanimble/streamhtml";
const { html, stable, live, hadIncompleteTag } = rehtml(partialHtml);
sanitizeHtml(html, config?)import { sanitizeHtml, configureSanitizer } from "@alphanimble/streamhtml";
configureSanitizer({ ALLOWED_TAGS: ["div", "p", "span", "table", ...] });
const safe = sanitizeHtml(untrustedHtml);
Tell your model to output semantic HTML with CSS classes:
142ms
avg latency
<table class="rt">
<thead><tr><th>Endpoint</th><th>Status</th></tr></thead>
<tbody>...</tbody>
</table>
<strong>Note:</strong> ...
Define component styles in your app — StreamHtml ships minimal base styles for tables, code, and typography.
git clone <your-repo-url>
cd htmlstream
npm install
npm test
npm run build
Gallery (static examples):
npm run demo
Chat (live OpenRouter streaming):
cp .env.example .env
# Add your OPENROUTER_API_KEY and optional MODEL_NAME
npm run chat
The chat demo streams HTML from OpenRouter and persists sessions locally in .chat-sessions/ (gitignored).
MIT — see LICENSE.
$ claude mcp add htmlstream \
-- python -m otcore.mcp_server <graph>