ngx-streamdown

January 19, 2026 ยท View on GitHub

Angular port of streamdown - A streaming markdown renderer optimized for AI-powered applications.

npm version

Overview

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.

Features

  • ๐Ÿš€ 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

Installation

npm install ngx-streamdown ngx-markdown

Peer Dependencies

  • @angular/common ^17.0.0 || ^18.0.0
  • @angular/core ^17.0.0 || ^18.0.0
  • ngx-markdown ^17.0.0 || ^18.0.0
  • rxjs ^7.8.0
  • remend (automatically installed)

Setup

1. Import Required Modules

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 {}

2. Add Styles (Optional)

Add to your angular.json or import in your global styles:

/* styles.css */
@import 'katex/dist/katex.css'; /* For math support */

Usage

Basic Example

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;
}

Streaming Example

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;
  }
}

With Real AI Streaming

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;
  }
}

Component API

Inputs

InputTypeDefaultDescription
contentstring''The markdown content to render
mode'static' | 'streaming''streaming'Rendering mode
parseIncompleteMarkdownbooleantrueApply remend to handle incomplete syntax
classNamestring''Additional CSS classes
enableKatexbooleantrueEnable KaTeX math rendering
enableMermaidbooleanfalseEnable Mermaid diagrams
isAnimatingbooleanfalseWhether content is currently streaming
showCaretbooleantrueShow cursor caret when streaming
caret'block' | 'bar' | 'underscore''block'Caret style
debounceTimenumber16Debounce time in ms (~60fps)
remendOptionsRemendOptionsundefinedOptions for remend parser
enableBlockParsingbooleantrueParse into blocks for progressive rendering

Service API

StreamingMarkdownService

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);
  }
}

Methods

  • processMarkdown(markdown: string, config?: StreamdownConfig): string - Process markdown with optional remend
  • parseIntoBlocks(markdown: string): string[] - Parse markdown into renderable blocks
  • hasIncompleteSyntax(markdown: string): boolean - Check if markdown has incomplete syntax
  • updateMarkdown(markdown: string, config?: StreamdownConfig): void - Update the markdown observable
  • reset(): void - Reset the markdown content

Styling

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;
}

Advanced Usage

Custom Remend Options

<ngx-streamdown
  [content]="markdown"
  [remendOptions]="{
    bold: true,
    italic: true,
    inlineCode: true,
    links: false,
    images: false
  }">
</ngx-streamdown>

Disable Block Parsing

For small content or when you want single-block rendering:

<ngx-streamdown
  [content]="markdown"
  [enableBlockParsing]="false">
</ngx-streamdown>

Custom Debounce Time

Adjust rendering frequency:

<ngx-streamdown
  [content]="markdown"
  [debounceTime]="50"> <!-- ~20fps -->
</ngx-streamdown>

Comparison with React Version

FeatureReact (streamdown)Angular (ngx-streamdown)
Streaming Supportโœ…โœ…
Remend Integrationโœ…โœ…
Block Parsingโœ…โœ…
KaTeX Supportโœ…โœ… (via ngx-markdown)
Mermaid Supportโœ…โœ… (via ngx-markdown)
Code HighlightingShikiPrism (ngx-markdown)
FrameworkReactAngular 17+

Performance Tips

  1. Use OnPush strategy - The component already uses OnPush change detection
  2. Adjust debounceTime - Lower values = smoother but more CPU intensive
  3. Disable block parsing for short content
  4. Use trackBy - Built-in for efficient ngFor rendering
  5. Virtual scrolling - For very long conversations, wrap in a virtual scroll container

Examples

See the examples directory for complete working examples:

  • streaming-example.component.ts - Basic streaming demonstration

Demo App

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

Development

# Install dependencies
npm install

# Build the library
npm run build

# Run tests
npm test

# Lint
npm run lint

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.