Angular port of streamdown - A streaming markdown renderer optimized for AI-powered applications.
ngx-streamdown brings the power of streaming markdown rendering to Angular applications. Built on top of ngx-markdown, it handles incomplete Markdown syntax gracefully during real-time streaming from AI models, providing seamless formatting even with partial or unterminated Markdown blocks.
- 🚀 Angular-native - Built with Angular signals and standalone components
- 🔄 Streaming-optimized - Handles incomplete Markdown gracefully using remend
- 🎨 Progressive rendering - Parses markdown into blocks for better performance
- 📊 GitHub Flavored Markdown - Full GFM support via ngx-markdown
- 🔢 Math rendering - LaTeX equations via KaTeX
- 🎯 TypeScript - Full type safety with TypeScript
- ⚡ Performance optimized - Debounced rendering and change detection
- 🛡️ OnPush strategy - Optimized change detection for better performance
npm install ngx-streamdown ngx-markdown@angular/common^17.0.0 || ^18.0.0@angular/core^17.0.0 || ^18.0.0ngx-markdown^17.0.0 || ^18.0.0rxjs^7.8.0remend(automatically installed)
For standalone components (Angular 14+):
// app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { provideMarkdown } from 'ngx-markdown';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideHttpClient(),
provideMarkdown(),
],
};For NgModule-based apps:
// app.module.ts
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { MarkdownModule } from 'ngx-markdown';
@NgModule({
imports: [
HttpClientModule,
MarkdownModule.forRoot(),
],
})
export class AppModule {}Add to your angular.json or import in your global styles:
/* styles.css */
@import 'katex/dist/katex.css'; /* For math support */import { Component } from '@angular/core';
import { StreamingMarkdownComponent } from 'ngx-streamdown';
@Component({
selector: 'app-chat',
standalone: true,
imports: [StreamingMarkdownComponent],
template: `
<ngx-streamdown
[content]="markdown"
[mode]="'streaming'"
[isAnimating]="isStreaming">
</ngx-streamdown>
`
})
export class ChatComponent {
markdown = '# Hello **World**!';
isStreaming = false;
}import { Component } from '@angular/core';
import { StreamingMarkdownComponent } from 'ngx-streamdown';
@Component({
selector: 'app-ai-chat',
standalone: true,
imports: [StreamingMarkdownComponent],
template: `
<div class="chat-container">
<ngx-streamdown
[content]="streamingContent"
[mode]="'streaming'"
[parseIncompleteMarkdown]="true"
[isAnimating]="isStreaming"
[showCaret]="true"
[caret]="'block'"
[enableKatex]="true">
</ngx-streamdown>
</div>
`
})
export class AIChatComponent {
streamingContent = '';
isStreaming = false;
async streamFromAI() {
this.isStreaming = true;
// Simulate streaming from an AI API
const fullResponse = "# AI Response\n\nThis is **streaming** from an AI!";
for (let i = 0; i < fullResponse.length; i++) {
this.streamingContent = fullResponse.substring(0, i + 1);
await new Promise(resolve => setTimeout(resolve, 50));
}
this.isStreaming = false;
}
}import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { StreamingMarkdownComponent } from 'ngx-streamdown';
@Component({
selector: 'app-ai-chat',
standalone: true,
imports: [StreamingMarkdownComponent],
template: `
<ngx-streamdown
[content]="response"
[mode]="'streaming'"
[isAnimating]="isStreaming">
</ngx-streamdown>
`
})
export class AIChatComponent {
response = '';
isStreaming = false;
constructor(private http: HttpClient) {}
async streamResponse(prompt: string) {
this.isStreaming = true;
this.response = '';
const response = await fetch('/api/ai/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt }),
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
if (!reader) return;
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
this.response += chunk;
}
this.isStreaming = false;
}
}| Input | Type | Default | Description |
|---|---|---|---|
content |
string |
'' |
The markdown content to render |
mode |
'static' | 'streaming' |
'streaming' |
Rendering mode |
parseIncompleteMarkdown |
boolean |
true |
Apply remend to handle incomplete syntax |
className |
string |
'' |
Additional CSS classes |
enableKatex |
boolean |
true |
Enable KaTeX math rendering |
enableMermaid |
boolean |
false |
Enable Mermaid diagrams |
isAnimating |
boolean |
false |
Whether content is currently streaming |
showCaret |
boolean |
true |
Show cursor caret when streaming |
caret |
'block' | 'bar' | 'underscore' |
'block' |
Caret style |
debounceTime |
number |
16 |
Debounce time in ms (~60fps) |
remendOptions |
RemendOptions |
undefined |
Options for remend parser |
enableBlockParsing |
boolean |
true |
Parse into blocks for progressive rendering |
Injectable service for processing markdown:
import { StreamingMarkdownService } from 'ngx-streamdown';
@Component({
// ...
})
export class MyComponent {
constructor(private streamingService: StreamingMarkdownService) {}
processMarkdown(text: string) {
// Process with remend
const processed = this.streamingService.processMarkdown(text, {
mode: 'streaming',
parseIncompleteMarkdown: true,
});
// Parse into blocks
const blocks = this.streamingService.parseIntoBlocks(processed);
// Check for incomplete syntax
const hasIncomplete = this.streamingService.hasIncompleteSyntax(text);
}
}processMarkdown(markdown: string, config?: StreamdownConfig): string- Process markdown with optional remendparseIntoBlocks(markdown: string): string[]- Parse markdown into renderable blockshasIncompleteSyntax(markdown: string): boolean- Check if markdown has incomplete syntaxupdateMarkdown(markdown: string, config?: StreamdownConfig): void- Update the markdown observablereset(): void- Reset the markdown content
The component includes default styles, but you can customize them using CSS variables:
ngx-streamdown {
--font-family: 'Inter', system-ui, sans-serif;
--font-size: 1rem;
--line-height: 1.6;
--text-color: #1f2937;
--link-color: #2563eb;
--code-bg: rgba(175, 184, 193, 0.2);
--border-color: #e5e7eb;
--table-header-bg: #f9fafb;
}Or use custom classes:
<ngx-streamdown
[content]="markdown"
className="my-custom-markdown">
</ngx-streamdown>.my-custom-markdown {
font-family: 'Georgia', serif;
font-size: 1.125rem;
}
.my-custom-markdown h1 {
color: #2563eb;
}<ngx-streamdown
[content]="markdown"
[remendOptions]="{
bold: true,
italic: true,
inlineCode: true,
links: false,
images: false
}">
</ngx-streamdown>For small content or when you want single-block rendering:
<ngx-streamdown
[content]="markdown"
[enableBlockParsing]="false">
</ngx-streamdown>Adjust rendering frequency:
<ngx-streamdown
[content]="markdown"
[debounceTime]="50"> <!-- ~20fps -->
</ngx-streamdown>| Feature | React (streamdown) | Angular (ngx-streamdown) |
|---|---|---|
| Streaming Support | ✅ | ✅ |
| Remend Integration | ✅ | ✅ |
| Block Parsing | ✅ | ✅ |
| KaTeX Support | ✅ | ✅ (via ngx-markdown) |
| Mermaid Support | ✅ | ✅ (via ngx-markdown) |
| Code Highlighting | Shiki | Prism (ngx-markdown) |
| Framework | React | Angular 17+ |
- Use OnPush strategy - The component already uses OnPush change detection
- Adjust debounceTime - Lower values = smoother but more CPU intensive
- Disable block parsing for short content
- Use trackBy - Built-in for efficient ngFor rendering
- Virtual scrolling - For very long conversations, wrap in a virtual scroll container
See the examples directory for complete working examples:
streaming-example.component.ts- Basic streaming demonstration
A full Angular demo app lives in demo-app. It showcases streaming controls, presets, and theming.
# Build the library first
npm install
npm run build
# Run the demo
cd demo-app
npm install
npm start# Install dependencies
npm install
# Build the library
npm run build
# Run tests
npm test
# Lint
npm run lint- streamdown - Original React version
- remend - Self-healing markdown parser
- ngx-markdown - Angular markdown component
MIT
Contributions are welcome! Please feel free to submit a Pull Request.