Skip to content

Repository files navigation

react-native-nitro-markdown

npm version npm downloads CI license React Native Expo Nitro Modules TypeScript

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.

Nitro Markdown rendering rich GitHub Flavored Markdown natively in React Native The same Markdown rendered with the built-in dark theme — fully customizable themes, per-node styles, and renderers

Benchmark comparing the Nitro C++ parser with JavaScript markdown parsers Streaming token-by-token markdown for LLM and chat output GitHub Flavored Markdown tables and task lists rendered natively

Why Nitro Markdown?

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).

Install

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 prebuild

react-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.

Quick start

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>.

Streaming (LLM / chat)

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.

Headless parsing

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.

Source AST rendering

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.

Theming & customization

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.

Common options

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.

Performance

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.

Security

  • 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 onLinkPress handlers receive the original href so apps can handle routes and custom schemes. The built-in Linking fallback opens only validated HTTP(S), mail, and telephone URLs. Remote images load by default for compatibility — set imageOptions={{ remoteImages: "deny" }} (and/or allowedHosts) 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.

Documentation

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.

Compatibility

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)

Contributing

bun install
bun run check          # lint + typecheck + tests
bun run example:ios    # run the example app

See CONTRIBUTING.md. Run native example builds before release when changing native, Nitro, rendering, or packaging files.

License

MIT

About

High-performance Markdown parser for React Native using Nitro Modules and md4c

Topics

Resources

Contributing

Security policy

Stars

308 stars

Watchers

2 watching

Forks

Releases

Contributors

Languages