react-native-markdown-stream
v0.1.4
Published
A lightweight React Native library for rendering live-updating Markdown content in real time
Readme
react-native-markdown-stream
A lightweight React Native renderer that turns streaming Markdown (chat responses, AI completions, docs) into polished UI in real time.
Highlights
- Streaming-first:
AsyncIterable,ReadableStream, generators, or manual chunk pushes all work out of the box. - Reveal controls: Animate output per chunk, word, or character with configurable delays.
- Theming without repainting the screen: ship light/dark presets, or merge your own colors with a simple config object.
- Code & math aware: inline/code blocks with optional copy actions, math rendering via the optional
react-native-math-viewpeer. - Zero native code: works in the classic architecture and the New Architecture (Fabric/TurboModules) without extra steps.
- Installation
- Metro configuration
- Quick start
- Streaming sources
- Component props
- Customising the renderer
- Theming
- Hook API
- Type exports
- Running the example
- Contributing
Installation
# yarn
yarn add react-native-markdown-stream
# npm
npm install react-native-markdown-streamOptional math support
Install the optional peer if you want LaTeX blocks to render with react-native-math-view:
yarn add react-native-math-viewWithout it installed, math blocks gracefully fall back to styled text.
Metro configuration
The library bundles modern ESM packages from the remark/unified ecosystem. Metro 0.72+ understands them as long as package exports are enabled.
Add or update metro.config.js in your app:
// metro.config.js
const {getDefaultConfig} = require('metro-config');
module.exports = (() => {
const config = getDefaultConfig(__dirname);
config.resolver.unstable_enablePackageExports = true;
return config;
})();Using Expo or a monorepo? Mirror the example app:
// example/metro.config.js
const path = require('path');
const {getDefaultConfig} = require('@expo/metro-config');
const {withMetroConfig} = require('react-native-monorepo-config');
const root = path.resolve(__dirname, '..');
const config = withMetroConfig(getDefaultConfig(__dirname), {
root,
dirname: __dirname,
});
config.resolver.unstable_enablePackageExports = true;
module.exports = config;Quick start
import {MarkdownStream} from 'react-native-markdown-stream';
const STREAM_URL = 'https://example.com/chat-stream';
export function ChatMessage() {
return (
<MarkdownStream
source={listenToSSE(STREAM_URL)}
revealMode="word"
revealDelay={24}
enableCodeCopy
enableImageLightbox
theme={{
base: 'dark',
colors: {
linkColor: '#4ade80',
quoteBorderColor: '#22c55e',
},
}}
/>
);
}
function* staticExample() {
yield '# Hello\n';
yield 'Streaming markdown arrives chunk by chunk.\n';
}
async function* listenToSSE(url: string) {
const response = await fetch(url);
if (!response.body) {
return;
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const {value, done} = await reader.read();
if (done) break;
yield decoder.decode(value);
}
reader.releaseLock();
}Controlled usage
Pass the content prop to render pre-computed Markdown (and still opt into streaming later if you call onReady controls).
<MarkdownStream content={markdownString} onReady={(controls) => controls.start()} />Supported stream sources
source accepts any of the following:
AsyncIterable<string>orIterable<string>(generators, async generators).- A function returning one of the above (lazy initialisation).
- A
ReadableStream(SSE,fetchwith streaming responses). - Call
controls.appendChunk()manually via theonReadycallback for complete control.
All chunks (strings, Uint8Array, objects with toString) are normalised to strings before parsing.
MarkdownStream props
| Prop | Type | Description |
| --- | --- | --- |
| source | MarkdownStreamSource<string> | Primary stream. Optional if you control content manually. |
| content | string | Fully rendered markdown. Bypasses streaming until you call start. |
| initialValue | string | Markdown shown before the first chunk arrives. |
| theme | 'light' \| 'dark' \| MarkdownTheme \| MarkdownThemeConfig | Pick a preset or merge custom colors; defaults to light theme with transparent background. |
| textColor | string | Override the resolved theme's primary text color without redefining the whole palette. |
| mutedTextColor | string | Override the resolved theme's muted/secondary text color. |
| revealMode | 'chunk' \| 'word' \| 'character' | Controls how new content animates in. |
| revealDelay | number | Delay (ms) between reveals; ignored when revealMode="chunk". |
| autoStart | boolean | Start streaming as soon as source exists (default true). |
| onReady | (controls: UseMarkdownStreamResult) => void | Exposes stream controls (append, reset, start, stop). |
| onChunk / onEnd / onError | callbacks | Tap into stream lifecycle events. |
| showCodeLineNumbers | boolean | Adds line numbers to fenced code blocks. |
| enableCodeCopy | boolean | Shows a copy action on code blocks (uses clipboard when available). |
| codeCopyLabel | string | Custom label for the copy button. |
| onCodeCopy | ({value, language}) => boolean \| void | Return true to mark the copy action as handled. Fires before the built-in clipboard logic. |
| onImagePress | ({url, alt}) => boolean \| void | Intercept image taps. Return true to skip built-in lightbox. |
| enableImageLightbox | boolean | Presents a modal preview when images are tapped. |
| onBlockLongPress | ({node}) => void | Receive long-press events for any block node. |
| blockLongPressDelay | number | Milliseconds before the long-press fires (default 300). |
| components | Partial<MarkdownRendererComponents> | Override individual renderers (codeBlock, inlineCode, mathBlock, image, …). |
Customising the renderer
Every major element exposes a customization hook:
- Provide
componentsto override code/math/image rendering with your own component tree. - Style code blocks via
CodeBlockcontainerStyle/codeStyleprops if you bring your own. - Hook into
onImagePress,enableImageLightbox, andonBlockLongPressfor richer media UX. - Implement your own copy logic with
onCodeCopy(e.g. analytics or custom tooltips).
Theming
Light and dark themes ship by default, both with transparent container backgrounds so they blend into your layout. Override a single token or a whole palette by passing a MarkdownTheme or MarkdownThemeConfig, or use the textColor / mutedTextColor props for quick tweaks.
import {MarkdownStream} from 'react-native-markdown-stream';
<MarkdownStream
theme={{
base: 'light',
colors: {
backgroundColor: 'transparent',
textColor: '#0f172a',
linkColor: '#f97316',
quoteBorderColor: '#fb923c',
},
}}
/>;Call resolveTheme if you need the concrete palette outside the component.
Handling incomplete markdown
Streaming text often arrives with half-typed emphasis or unfinished links. Before parsing we run the content through a small sanitizer that auto-closes common Markdown markers and neutralises broken URLs. The implementation is adapted from Vercel's streamdown parse-incomplete-markdown (Apache-2.0).
Using useMarkdownStream directly
The hook powers the component and can run headless when you need custom rendering.
import {useMarkdownStream} from 'react-native-markdown-stream';
export function CustomRenderer({source}) {
const stream = useMarkdownStream({
source,
revealMode: 'character',
onChunk: (chunk) => console.log('chunk', chunk.length),
});
return (
<ScrollView>
<Text>{stream.content}</Text>
<Button title="Skip animation" onPress={() => stream.setRevealMode('chunk')} />
</ScrollView>
);
}The hook returns the current content, the accumulated fullContent, status flags, and control helpers (appendChunk, reset, start, stop, setRevealMode, setRevealDelay).
Other exports
import {
MarkdownRenderer,
parseMarkdown,
useMarkdownStream,
lightTheme,
darkTheme,
resolveTheme,
type MarkdownTheme,
type MarkdownThemeConfig,
type ThemeMode,
type MarkdownRendererComponents,
} from 'react-native-markdown-stream';Use MarkdownRenderer when you already have an mdast Root, or to wrap custom parsed content.
Running the example app
yarn install
yarn example # starts Expo in the example/ workspace
# or target a platform directly
yarn workspace react-native-markdown-stream-example android
yarn workspace react-native-markdown-stream-example iosThe example showcases reveal modes, code copy actions, image lightbox, and long-press handlers.
Architecture support
The package is JavaScript-only and works unchanged in both the classic bridge and the New Architecture (Fabric/TurboModules). No native modules, pods, or Gradle steps are required.
