streamdown-rn
v0.2.0
Published
High-performance streaming markdown renderer for React Native with dynamic component injection
Maintainers
Readme
streamdown-rn
High-performance streaming markdown renderer for React Native, optimized for AI responses.
Features
- Streaming-first — Renders markdown as it arrives, character by character
- Format-as-you-type — Formatting appears immediately (e.g.,
**boshows as bo) - Progressive components — Custom components stream with skeleton placeholders
- Block-level memoization — Stable blocks never re-render
- Full GFM support — Tables, strikethrough, task lists via remark-gfm
- Syntax highlighting — Prism-based code highlighting
Installation
npm install streamdown-rn
# or
bun add streamdown-rnPeer Dependencies
{
"react": "^19.0.0",
"react-native": "^0.81.0"
}Basic Usage
import { StreamdownRN } from 'streamdown-rn';
function ChatMessage({ content }: { content: string }) {
return (
<StreamdownRN theme="dark">
{content}
</StreamdownRN>
);
}Custom Components
Inject custom React Native components using the [{c:"Name",p:{...}}] syntax:
import { StreamdownRN, type ComponentRegistry, type ComponentDefinition } from 'streamdown-rn';
// Define your component
const StatusCard = ({ title, status }) => (
<View style={styles.card}>
<Text>{title}</Text>
<Text>{status}</Text>
</View>
);
// Create a registry
const registry: ComponentRegistry = {
get: (name) => definitions[name],
has: (name) => !!definitions[name],
validate: () => ({ valid: true, errors: [] }),
};
const definitions: Record<string, ComponentDefinition> = {
StatusCard: {
component: StatusCard,
skeletonComponent: StatusCardSkeleton, // Optional: shown while streaming
},
};
// Use it
<StreamdownRN componentRegistry={registry}>
{`Here's a status card:
[{c:"StatusCard",p:{"title":"Build Status","status":"passing"}}]
More text below.`}
</StreamdownRN>Component Syntax
Components use a compact JSON syntax:
[{c:"ComponentName",p:{"prop1":"value","prop2":123}}]c— Component name (must exist in registry)p— Props object (JSON)
Progressive Prop Streaming
Components render progressively as props stream in. Define a skeletonComponent to show placeholders for missing props:
const StatusCardSkeleton = ({ title, status }) => (
<View style={styles.card}>
{title ? <Text>{title}</Text> : <SkeletonText width={100} />}
{status ? <Text>{status}</Text> : <SkeletonText width={60} />}
</View>
);Skeleton Primitives
Build skeleton components using provided primitives:
import { Skeleton, SkeletonText, SkeletonCircle, SkeletonNumber } from 'streamdown-rn';
// Basic rectangle
<Skeleton width={100} height={20} />
// Text placeholder (single or multi-line)
<SkeletonText width={200} lines={3} gap={6} />
// Circle (for avatars)
<SkeletonCircle size={40} />
// Number placeholder
<SkeletonNumber width={50} />Theming
<StreamdownRN theme="dark"> {/* or "light" */}
{content}
</StreamdownRN>Debugging
Enable debug callbacks to inspect the streaming state:
<StreamdownRN
onDebug={(snapshot) => {
console.log('Blocks:', snapshot.registry.stableBlockCount);
console.log('Active:', snapshot.registry.activeBlock?.type);
}}
isComplete={streamingDone}
>
{content}
</StreamdownRN>API Reference
StreamdownRN Props
| Prop | Type | Description |
|------|------|-------------|
| children | string | Markdown content to render |
| componentRegistry | ComponentRegistry | Custom component definitions |
| theme | 'dark' \| 'light' | Color theme (default: 'dark') |
| onDebug | (snapshot: DebugSnapshot) => void | Debug callback |
| onError | (error: Error) => void | Error handler |
| isComplete | boolean | Set true when streaming finishes |
Security
streamdown-rn includes built-in protection against XSS attacks:
URL Sanitization
All URLs in markdown links, images, and component props are automatically sanitized using an allowlist approach. Only these protocols are permitted:
http:,https:— Web URLsmailto:— Email linkstel:,sms:— Phone links- Relative URLs (
/path,#anchor,./file)
Blocked protocols (examples):
javascript:— Script executiondata:— Inline data (can contain scripts)vbscript:— Legacy script executionfile:— Local file access
Component Props
Component props from the [{c:"Name",p:{...}}] syntax are sanitized recursively. Any URL-like string values are checked against the allowlist.
// This malicious input:
[{c:"Card",p:{"url":"javascript:alert(1)"}}]
// Results in sanitized props:
{ url: '' } // Dangerous URL replaced with empty stringHTML in Markdown
Raw HTML in markdown (e.g., <script>alert(1)</script>) is rendered as plain text, not executed. We never use dangerouslySetInnerHTML.
Using Sanitization Utilities
You can use the sanitization functions directly if needed:
import { sanitizeURL, sanitizeProps } from 'streamdown-rn';
// Sanitize a single URL
sanitizeURL('javascript:alert(1)'); // null
sanitizeURL('https://example.com'); // 'https://example.com'
// Sanitize an object with URL props
sanitizeProps({ href: 'javascript:evil()', title: 'Safe' });
// { href: '', title: 'Safe' }Architecture
See ARCHITECTURE.md for detailed implementation notes.
License
Apache-2.0
