streamdown-angular
streamdown-angular
Stream-safe Markdown rendering for Angular — built for AI chat UIs
Angular port of Vercel Streamdown. Drop‑in <ngx-streamdown> component powered by signals, standalone components and OnPush.
Render Markdown that arrives one token at a time. streamdown-angular parses Markdown into a sanitized HAST tree and renders it as real Angular DOM (never innerHTML of untrusted content). As text streams in, incomplete Markdown — **bold, [half link, unterminated code fences — is auto‑completed, and a diffing renderer updates only what changed (no flicker, scroll & selection preserved).
<ngx-streamdown [content]="response()" [caret]="true" />Contents
- Why streamdown-angular
- Install
- Quick start
- Streaming from an API
- Inputs
- Optional plugins
- Styling
- Internationalization & customization
- Low‑level API
- How it works
- License
Why streamdown-angular
| Feature | |
|---|---|
Streaming‑first — Markdown is split into memoized blocks; only the changed block re‑renders. Incomplete Markdown is fixed via remend. |
|
| Incremental rendering — HAST diffing keeps unchanged DOM in place; no flicker, scroll/selection/focus preserved, mapped components keep their instance. | |
Safe by default — rehype-sanitize in the pipeline; text via Renderer2.createText, never raw innerHTML. |
|
| GFM — tables, strikethrough, task lists, autolinks. | |
| Code blocks — Shiki highlighting (light/dark) + copy & download buttons. Default on. | |
| Tables — copy as Markdown/CSV, download CSV. Default on. | |
Math — KaTeX $inline$ and $block$. Opt‑in. |
|
| Diagrams — Mermaid. Opt‑in. | |
| Streaming caret + optional fade‑in for new tokens. | |
Link safety — external links open via a confirmation modal; always rel="noopener noreferrer". |
|
| Images — loading skeleton + error fallback. | |
| i18n / icons / prefix — fully overridable via providers. | |
| RTL/LTR auto‑detection. |
Install
npm i streamdown-angularHeavy features are optional peer dependencies — install only what you use:
npm i shiki # code highlighting (recommended)
npm i mermaid # diagrams
npm i katex remark-math rehype-katex # mathRequires Angular 21+.
Quick start
import { Component, signal } from '@angular/core';
import { StreamdownComponent } from 'streamdown-angular';
@Component({
selector: 'app-chat',
standalone: true,
imports: [StreamdownComponent],
template: `<ngx-streamdown [content]="markdown()" />`,
})
export class ChatComponent {
markdown = signal('# Hello 👋\n\nStreaming **markdown** with `code`, tables and math.');
}That's it — no module imports, no NgModule. Code blocks and tables work out of the box.
Streaming from an API
Just keep growing the content signal as chunks arrive. Turn on caret while the response is in flight:
@Component({
selector: 'app-chat',
standalone: true,
imports: [StreamdownComponent],
template: `<ngx-streamdown [content]="answer()" [caret]="streaming()" />`,
})
export class ChatComponent {
readonly answer = signal('');
readonly streaming = signal(false);
async ask(prompt: string): Promise<void> {
this.answer.set('');
this.streaming.set(true);
for await (const chunk of streamFromApi(prompt)) {
this.answer.update((text) => text + chunk); // diffing renders only the delta
}
this.streaming.set(false);
}
}Inputs
| Input | Type | Default | Description |
|---|---|---|---|
content |
string |
'' |
Markdown source — grow it as tokens stream in. |
parseIncompleteMarkdown |
boolean |
true |
Auto‑complete unterminated Markdown while streaming. |
caret |
boolean |
false |
Blinking "typing" caret after the last block. |
Optional plugins
Register once in your app providers:
import { ApplicationConfig } from '@angular/core';
import {
provideStreamdownMath,
provideStreamdownMermaid,
provideStreamdownAnimation,
} from 'streamdown-angular';
export const appConfig: ApplicationConfig = {
providers: [
provideStreamdownMath(), // KaTeX: $E=mc^2$ and $\int_0^\infty …$
provideStreamdownMermaid(), // ```mermaid fenced diagrams
provideStreamdownAnimation(), // fade-in newly streamed elements
],
};Math needs KaTeX CSS. Add it to
angular.json→styles:"node_modules/katex/dist/katex.min.css"
| Provider | Adds | Extra dependency |
|---|---|---|
provideStreamdownMath() |
KaTeX math | katex, remark-math, rehype-katex |
provideStreamdownMermaid() |
Mermaid diagrams | mermaid |
provideStreamdownAnimation() |
Fade‑in for new DOM (respects prefers-reduced-motion) |
— |
provideStreamdownLinkSafety() |
Confirmation modal for external links | — |
Styling
Zero‑config — no Tailwind, no CSS import needed. All styles are plain CSS, scoped under
.ngx-streamdown, and ship inside the component (ViewEncapsulation.None). They apply
automatically when the component is used and never leak into the rest of your app.
Customize by overriding the scoped CSS in your own stylesheet:
.ngx-streamdown h1 { font-size: 2.25rem; color: #111; }
.ngx-streamdown a { color: rebeccapurple; }Or add classes per tag / swap whole components from TypeScript:
import { ELEMENT_CLASSES, COMPONENT_MAP } from 'streamdown-angular';
ELEMENT_CLASSES['h1'] = 'my-heading'; // extra class on every <h1>
COMPONENT_MAP.set('pre', MyCodeBlockComponent); // replace the code-block rendererInternationalization & customization
import {
provideStreamdownTranslations,
provideStreamdownIcons,
provideStreamdownPrefix,
} from 'streamdown-angular';
providers: [
// i18n — every UI string (e.g. Uzbek)
provideStreamdownTranslations({
copy: 'Nusxa', copied: 'Nusxalandi', download: 'Yuklab olish',
linkWarningTitle: 'Tashqi havolani ochasizmi?', open: 'Ochish', cancel: 'Bekor qilish',
}),
// swap the copy / download / … SVG icons
provideStreamdownIcons({ copy: '<svg>…</svg>' }),
// namespace CSS hooks
provideStreamdownPrefix('myapp'),
]Low‑level API
Render Markdown or HAST yourself
| Export | Purpose |
|---|---|
MarkdownService.toHast(md, { remend? }) |
Markdown → sanitized HAST Root |
HastRendererComponent |
Renders a HAST Root to Angular DOM (with diffing) |
HastRendererService |
The reconciling HAST → DOM engine (reconcile, renderNodes, clear) |
parseMarkdownIntoBlocks(md) |
Split Markdown into top‑level blocks |
detectDirection(text) |
'rtl' / 'ltr' |
extractTableData · toCsv · toTsv · toMarkdown |
Table export helpers |
STREAMDOWN_PIPELINE |
DI token to add custom remark/rehype plugins to the pipeline |
const md = inject(MarkdownService);
const hast = md.toHast('# Hi'); // → HAST RootHow it works
Data flow (React → Angular mapping)
content ──▶ parseMarkdownIntoBlocks ──▶ @for(block) [track $index, OnPush]
└─▶ MarkdownService.toHast
remark-parse → gfm → [plugins] → remark-rehype
→ rehype-raw → [plugins] → rehype-sanitize
└─▶ HastRendererComponent ──▶ HastRendererService.reconcile
├─ text node → Renderer2.createText (patched in place)
├─ generic tag → Renderer2.createElement (attrs diffed)
└─ pre/table/img→ Angular component (node input updated)
It mirrors Streamdown's React data flow — useMemo chain → computed, memo() → OnPush + @for track — and replaces React's hast-util-to-jsx-runtime with a native, diffing Angular DOM renderer.
License
Apache‑2.0 — Angular port of Streamdown. Original work 2023 Vercel, Inc. (see NOTICE).