@preferred-markdown-stream/react
v0.6.0
Published
Streaming markdown renderer for React with word-level fade-in, incremental rendering, shiki and katex
Downloads
70
Readme
@preferred-markdown-stream/react
React bindings for streaming Markdown rendering.
- Frozen-prefix incremental rendering: settled blocks are reused by element identity, so React bails out of reconciling them and per-chunk cost stays flat as the document grows.
- Incomplete Markdown protection: tables, math, links, and inline markup never flash as raw text mid-stream.
- Word-level fade-in (per CJK character) that never re-animates already-rendered text.
- Lazy Shiki (per-language grammar loading) and lazy KaTeX.
Installation
pnpm add @preferred-markdown-stream/react reactUsage
import { StreamingMarkdown } from '@preferred-markdown-stream/react'
import '@preferred-markdown-stream/react/styles.css'
function ChatMessage({ text, isStreaming }: { text: string, isStreaming: boolean }) {
return <StreamingMarkdown content={text} loading={isStreaming} />
}Or as a hook, when you want to place the nodes yourself:
import { useStreamingMarkdown } from '@preferred-markdown-stream/react'
function ChatMessage({ text, isStreaming }: { text: string, isStreaming: boolean }) {
const node = useStreamingMarkdown(text, isStreaming)
return <div className="message">{node}</div>
}For bursty token streams, smooth the reveal first:
import { useSmoothedContent, useStreamingMarkdown } from '@preferred-markdown-stream/react'
const { content } = useSmoothedContent(rawStreamedText)
const node = useStreamingMarkdown(content, isStreaming)Customizing the fade animation
The built-in animation is driven by CSS variables — override them on any ancestor:
.chat-message {
--preferred-markdown-stream-animation-duration: 0.4s;
--preferred-markdown-stream-animation-timing-function: ease-out;
/* --preferred-markdown-stream-animation-name: your-own-keyframes; */
}To integrate with an existing design system, replace the class entirely and
bring your own animation CSS via the fadeInClassName option.
The default code block UI is themeable the same way via
--preferred-markdown-stream-code-* variables (bg, radius, font,
toolbar-bg, toolbar-color, copy-hover-color).
Public API
StreamingMarkdown— component form; props:content,loading, plus the hook options belowuseStreamingMarkdown(content, loading, options?)— options:splitMode,fadeInClassName,fadeInSegment,collapseDelayMsuseSmoothedContent(source, options?)— typewriter smoothing: drains bursty chunks into a steady per-character reveal with proportional catch-upconfigureMermaid(loader, config?)— opt into diagram rendering, e.g.configureMermaid(() => import('mermaid')); mermaid fences render nothing while streaming and swap to SVG once a prefix parsesconfigureShiki(options)/loadShiki(options?)/loadKatex()setCodeBlockComponent(component)— defaults toDefaultCodeBlock(language label + copy button); receives{ language, content, preAttrs }wherecontentis highlighted HTMLsetCustomComponents({ a: MyLink, img: MyImage, ... })— replace rendered tags with your components; attributes arrive as props (classmapped toclassName), content throughprops.childrensplitContent(message)/stripIncompleteMarkdown(message)
Notes
- The heavy lifting lives in
@preferred-markdown-stream/renderer, shared with the Vue package; this package contributes the React adapter, components, and hooks. - Default animation styles are published via
@preferred-markdown-stream/react/styles.css. - KaTeX and Shiki are loaded on demand when matching content is detected.
- The fade-in state is intentionally delayed for about 1 second after loading completes so the last rendered batch can still animate.
- Raw HTML rendering uses browser DOM APIs internally, so client-side rendering is the safest path when your Markdown may contain raw HTML.
