The fast Markdown engine for React Native. Native C++ parsing (CommonMark
- GitHub Flavored Markdown), real React Native rendering, first-class streaming for LLM/chat output, and a headless AST API — powered by md4c and Nitro Modules.
Most React Native Markdown libraries parse in JavaScript on the JS thread. Nitro Markdown parses in a native C++ engine over JSI, then renders flexible React Native components — so you get native parse speed and component flexibility.
- ⚡ Native C++ parsing — ~2.8× to ~19× faster than JS parsers (benchmarks).
- 🔀 Streaming — built for token-by-token LLM / chat output.
- 🧩 Headless AST — parse without UI for search, validation, indexing.
- 🎨 Real components — theme, override per node, or swap whole renderers.
- 📜 Virtualization — bounded memory and fast first screen on long docs.
- 📊 GFM tables, task lists, inline & block math, syntax highlighting built in.
- 🛡️ Type-safe — full TypeScript types for nodes, renderers, options.
- 🔒 Safe by default — bounded parse input (default 10M chars, overridable via
options.maxInputLength), a hard C++ cap, seeded fuzzing and a CommonMark/GFM conformance corpus in the test gate, and a link/image URL policy (security policy).
bun add react-native-nitro-markdown react-native-nitro-modules@0.36.5 ratex-react-native@0.1.14# Expo development build
bunx expo install react-native-nitro-markdown react-native-nitro-modules@0.36.5 ratex-react-native@0.1.14
bunx expo prebuildreact-native-nitro-modules and ratex-react-native are peer dependencies
(parsing and math rendering use native code). Expo Go cannot load Nitro
modules — use a development build. Full guide: Installation.
import { Markdown } from "react-native-nitro-markdown";
export function Article() {
return (
<Markdown
options={{ gfm: true, math: true }}
onError={(error) => {
console.error(error);
}}
>
{"# Hello\nThis is **native** markdown."}
</Markdown>
);
}Native parse failures call onError instead of rendering an empty document.
Headless parseMarkdown throws; do not treat an empty AST as success. Keep
product fonts and colors in an app wrapper around <Markdown>.
import { useEffect } from "react";
import { MarkdownStream, useMarkdownSession } from "react-native-nitro-markdown";
type StreamingMessageProps = {
subscribe: (onToken: (token: string) => void) => () => void;
onError: (error: Error) => void;
};
export function StreamingMessage({
subscribe,
onError,
}: StreamingMessageProps) {
const session = useMarkdownSession();
useEffect(
() => subscribe((token) => session.getSession().append(token)),
[session, subscribe],
);
return (
<MarkdownStream
session={session}
updateStrategy="raf"
incrementalParsing
onError={onError}
/>
);
}MarkdownStream batches native range updates. Plain-text and fenced-code
appends take an incremental path; structural updates re-parse with stable AST
node reuse. Failed updates call onError(error, "parse") and retain the last
valid render. For very large initial content, pass initialParseMode="async"
so the first frame renders without parsing. Full guide:
Streaming.
import {
parseMarkdown,
parseMarkdownWithOptions,
extractPlainText,
} from "react-native-nitro-markdown/headless";
const ast = parseMarkdown("# Title");
const mathAst = parseMarkdownWithOptions("Inline $x^2$", { math: true });
const text = extractPlainText("Hello **world**"); // "Hello world"
// Search / indexing: skip source offsets natively for a leaner, faster AST.
const lean = parseMarkdownWithOptions(doc, { sourceOffsets: false });Use the /headless export for AST data, plain-text extraction, indexing, or
tests without rendering UI. Parser functions throw when the native module is
unavailable, parsing fails, or native output is invalid; catch errors at your
application boundary. The headless entry still requires an iOS or Android native
runtime. Full guide: Headless.
Already have a MarkdownNode? Pass it via sourceAst to skip native parsing on
render:
<Markdown sourceAst={ast}>{"# Cached AST"}</Markdown>When sourceAst is provided, beforeParse plugins are skipped because parsing
already happened. afterParse plugins and astTransform still run.
Because every node renders as a real React Native component, you can restyle the whole document, tweak a single node type, or replace a renderer outright:
import { Markdown, darkMarkdownTheme } from "react-native-nitro-markdown";
// 1. Swap the whole theme — built-in dark preset (or any partial theme)
<Markdown theme={darkMarkdownTheme}>{content}</Markdown>;
// 2. Override individual node styles (layered on top of the theme)
<Markdown styles={{ heading: { color: "#7c3aed" }, code_block: { borderRadius: 16 } }}>
{content}
</Markdown>;
// 3. Replace a renderer entirely
<Markdown renderers={{ blockquote: MyCallout }}>{content}</Markdown>;Presets: defaultMarkdownTheme, darkMarkdownTheme, minimalMarkdownTheme (or
stylingStrategy="minimal"). Compose with mergeThemes. Full guide:
Customization.
| Prop / option | Default | What it does |
|---|---|---|
options.gfm |
true |
Tables, strikethrough, task lists, autolinks. |
options.math |
true |
Inline and block math nodes. |
options.html |
false |
Preserve raw HTML nodes for custom renderers. |
options.sourceOffsets |
true |
Emit per-node beg/end source offsets as JavaScript UTF-16 indices, matching String.length and String.slice. Set false for one-shot headless parses to shrink the AST and speed up the round trip (the native parser skips the offset map entirely). |
options.maxInputLength |
10000000 |
Maximum accepted input length in characters. Oversized inputs fail with a typed input_too_large error instead of being parsed. |
parseCache |
true |
Reuse parsed ASTs for repeated content. The cache is scoped per <Markdown> instance (max 32 entries); per-instance hit/miss/eviction counters are reported via onParseComplete's cacheStats. |
sourceAst |
undefined |
Render a pre-parsed AST instead of parsing children. |
onError |
undefined |
Receive parser and plugin failures as (error, phase, pluginName?). Native parse and session failures are typed MarkdownErrors with stable code and source. |
errorText |
"Error parsing markdown" |
Localized text rendered when parsing fails. |
imageOptions |
undefined |
Image URL policy: allowedProtocols, allowedHosts, and remoteImages: "deny" to block remote image loading entirely. |
highlightCode |
false |
Built-in code syntax highlighting (fixture-backed languages: JS/TS family, Python, shell). |
virtualize |
false |
Virtualize top-level blocks for long documents. |
See Usage for the full prop table and Customization for themes, per-node styles, custom renderers, and plugins.
Parsing a ~320 KB document (example app, iOS Simulator; ratios are stable):
| Parser | Time | vs Nitro |
|---|---|---|
| Nitro (C++) | ~41 ms | — |
| CommonMark (JS) | ~113 ms | ~2.8× |
| Markdown-It (JS) | ~184 ms | ~4.5× |
| Marked (JS) | ~814 ms | ~19.8× |
Reproduce it: run the example app and tap Run Benchmark. Methodology and a full capability matrix: Comparison & benchmarks.
- Parse input is bounded: the JavaScript boundary rejects documents above
options.maxInputLength(default 10M characters) with a typed error, and the C++ parser enforces the same hard cap in bytes plus a 64 MB JSON output cap. - Custom
onLinkPresshandlers receive the original href so apps can handle routes and custom schemes. The built-inLinkingfallback opens only validated HTTP(S), mail, and telephone URLs. Remote images load by default for compatibility — setimageOptions={{ remoteImages: "deny" }}(and/orallowedHosts) when rendering untrusted markdown in privacy- or SSRF-sensitive apps. - The C++ parser is fuzzed with a seeded, deterministic corpus and checked
against a CommonMark/GFM conformance corpus in
bun run check.
See SECURITY.md for supported versions and how to report issues.
| Guide | What's inside |
|---|---|
| Installation | Expo & bare RN setup, requirements, platforms. |
| Usage | <Markdown>, props, elements, virtualization, source AST. |
| Streaming | Token-by-token LLM / chat rendering. |
| Headless | Parse to AST, plain-text extraction. |
| Customization | Themes, dark mode, per-node styles, renderers, plugins. |
| Comparison & benchmarks | Why Nitro, parse benchmarks, capability matrix. |
| API reference | Full export and type listing. |
| Security policy | Supported versions, link/image policy, reporting. |
| Changelog | Package changes and migration requirements by version. |
| Troubleshooting | Common install and runtime issues. |
| Dependency | Supported |
|---|---|
| React Native | >=0.75 (New Architecture) |
| Nitro Modules | >=0.36.5 <0.37.0 |
| RaTeX React Native | >=0.1.4 (example validated with 0.1.14) |
| Expo | SDK 57 development builds |
| Platforms | iOS, Android (Web not supported) |
bun install
bun run check # lint + typecheck + tests
bun run example:ios # run the example appSee CONTRIBUTING.md. Run native example builds before release when changing native, Nitro, rendering, or packaging files.




