Skip to main content

Native assets

Two kinds of native assets matter when you ship Enriched Markdown: the fonts and images you bundle into your app and reference from Markdown, and the large prebuilt assets the library itself downloads at install time for LaTeX math and code highlighting. This guide covers both - how to reference your own assets, and how the library's assets are fetched and how to opt out of them to shrink your binary.

Custom fonts​

Font resolution goes through React Native's font manager (RCTFont on iOS, ReactFontManager on Android), so any font bundled the normal React Native way is available to markdownStyle. There are two steps: bundle the font, then reference its family name.

note

The iOS editor is the one exception: EnrichedMarkdownTextInput resolves its font with UIFont fontWithName: rather than through RCTFont, and silently keeps the system font when the name does not match. Setting fontWeight and fontFamily together on the same style there also loses the weight.

Bundle the font​

Use the expo-font config plugin to embed the fonts into the native build. Embedding (rather than the runtime useFonts hook) makes the fonts available immediately on startup, which is what the native renderer needs:

npx expo install expo-font
// app.json
{
"expo": {
"plugins": [
[
"expo-font",
{
"fonts": [
"./assets/fonts/Montserrat-Regular.ttf",
"./assets/fonts/Montserrat-Bold.ttf"
]
}
]
]
}
}

Then rebuild the native projects:

npx expo prebuild --clean
npx expo run:ios # or: npx expo run:android

The plugin also accepts platform-specific lists. Per-weight fontDefinitions are Android only (android.fonts[].fontDefinitions); the ios key takes paths and always derives the family from the file. See the Expo fonts guide.

Reference the font​

Set fontFamily anywhere markdownStyle accepts typography - it cascades from a block to its inline children. Use the same family name you would pass to a React Native <Text> (the font's PostScript / family name, e.g. Montserrat-Regular):

<EnrichedMarkdownText
markdown={'# Heading\n\nBody copy set in a **custom** font.'}
markdownStyle={{
paragraph: { fontFamily: 'Montserrat-Regular' },
h1: { fontFamily: 'Montserrat-Bold' },
}}
/>

The same fontFamily works in EnrichedMarkdownTextInput's style prop. For which style keys accept fontFamily and how it interacts with bold and italic, see Style properties.

note

Single-file custom fonts (one face, no bundled _bold / _italic variants) behave differently per platform. Android loads the family at its regular weight and synthesizes bold and italic on top, so **bold** and *italic* still render in your font. iOS matches a real bold or italic face through CoreText and, when none exists, falls back to the unchanged font - so a truly single-face font can render **bold** not bold on iOS. To use the face exactly as-is, set fontWeight / fontStyle to 'normal' on strong / em - note that this only takes effect when fontFamily is also set on that same style key (see the Style properties link above).

caution

Multi-weight families are the common gotcha on Android: the library always loads the family at Typeface.NORMAL and synthesizes, so a bundled Montserrat-Bold.ttf is never auto-selected for **bold** even when it is present. iOS does pick the real face. The portable fix is to name the face you want per style key - strong: { fontFamily: 'Montserrat-Bold' }, em: { fontFamily: 'Montserrat-Italic' } - rather than relying on synthesis.

note

None of this applies on web, where fontFamily is passed straight through to CSS. A font bundled the React Native way is invisible to the browser; declare it with an @font-face rule instead.

Bundled images​

Markdown image URLs are plain strings, so a require()d image must be resolved to a URI before it goes into the Markdown:

import { Image } from 'react-native';

const logoUri = Image.resolveAssetSource(require('./assets/logo.png')).uri;
const markdown = `![Logo](${logoUri})`;

This works in development (a Metro dev-server URL) and in release builds (a drawable resource on Android, a bundle file:// URL on iOS). For the full list of supported image sources per platform - and how images are cached - see Image caching.

Install-time native downloads​

Two parts of the library's native layer are large prebuilt assets: the tree-sitter runtime and grammar sources for code-block highlighting and the RaTeX static XCFramework for iOS LaTeX math. Shipping them inside the npm tarball would push every install past 200 MB, even for apps that use neither feature.

Instead they are downloaded once at install time by a postinstall script, keeping the published package small. The tree-sitter runtime tarball and both RaTeX assets are pinned and verified by sha256; the grammar sources come from the npm registry at pinned versions. Every download is idempotent - a .stamp fingerprint in each vendor directory makes repeated installs a no-op.

What gets downloaded​

AssetSourceUsed by
tree-sitter runtimeGitHub release tarballcode highlighting (iOS + Android)
grammar sources (parser.c / scanner.c / highlights.scm)npm registrycode highlighting (iOS + Android)
RaTeX XCFramework + fontsGitHub releaseLaTeX math (iOS only)

Files land under node_modules/react-native-enriched-markdown/ (cpp/highlight/vendor/ for grammars and runtime, ios/vendor/ for RaTeX). Because this runs during npm install / yarn add, the assets are in place before pod install or the Android build reads them.

Requirements​

  • Network access at install time to registry.npmjs.org and github.com.
  • tar (present on macOS, Linux, and Windows 10+). RaTeX also needs unzip, which is absent on stock Windows - but RaTeX is iOS-only, so this only affects Windows dev machines and is non-fatal.

The postinstall step never fails the install: if a download does not complete it prints a warning and exits successfully. What the native build does with a missing asset depends on whether you explicitly enabled the feature:

  • On by default (no enriched-markdown entry): the build treats the feature as disabled - code highlighting compiles a no-op stub, iOS math is skipped with a CocoaPods warning - so the build stays green.
  • Explicitly enabled ("enableMath": true / "enableCodeHighlight": true): the build fails with an actionable error telling you to re-run postinstall.

Recovering a failed or skipped download​

If a download didn't complete (offline, behind a firewall, or with scripts disabled), reinstall the package to re-fetch the assets:

npm rebuild react-native-enriched-markdown

If your package manager blocks or skips that (pnpm, Yarn PnP, --ignore-scripts), run the vendor script directly from your project root:

node node_modules/react-native-enriched-markdown/postinstall.mjs

Then re-run pod install (iOS) or rebuild (Android).

To see what the postinstall decided without downloading anything, run it with --resolve-config from the directory the install ran from. It prints the workspace members it found and the merged enriched-markdown block ({} means download everything):

INIT_CWD=$PWD node node_modules/react-native-enriched-markdown/postinstall.mjs --resolve-config
note

Package managers. npm and Yarn (node-modules linker) work out of the box. pnpm blocks dependency lifecycle scripts by default - allow this package to run its postinstall by adding it to onlyBuiltDependencies, either in pnpm-workspace.yaml or in the "pnpm" field of your package.json. pnpm also caches postinstall output (sideEffectsCache, on by default) by package and platform, not by your enriched-markdown block, so a project that opted out of an asset can leave the next project on the same machine without it; run the recovery command above if so. Yarn PnP is not supported: its read-only archives can't receive the vendored assets, so use the node-modules linker (the norm for React Native anyway). With --ignore-scripts, run the recovery command above after installing.

Reducing binary size (opt out)​

If you don't use a feature, opt out in your app's package.json so the postinstall never downloads its assets and the native build never links them. Add an enriched-markdown block (both fields default to true):

{
"enriched-markdown": {
"enableCodeHighlight": false,
"enableMath": false
}
}

enableCodeHighlight: false skips the tree-sitter runtime and grammar download (iOS and Android). enableMath: false skips the RaTeX download, which is an iOS-only asset - but the same flag also turns math off at build time on Android, where it drops the io.github.erweixin:ratex-android dependency and the math source set. The native build reads the same block directly, so a package.json opt-out disables the feature on both platforms - no Podfile or gradle.properties edit needed.

important

Re-enabling a feature needs a reinstall first. The assets are only fetched during postinstall, so flipping a flag back to true and going straight to pod install or a Gradle build hits the explicit-opt-in hard-fail branch (the feature is requested but its assets are absent). Re-run the install - or npm rebuild react-native-enriched-markdown - before building.

Applying a change on iOS. The podspec reads package.json at pod install time, so run pod install after editing the block. When you disable a feature that was already compiled in, also do a clean build (Product > Clean Build Folder, or delete the app's DerivedData): Xcode's incremental build does not reliably rebuild the pod's static library when only its source list changes. Android reconfigures on every build and needs neither step.

Which package.json? The download opt-out is read from wherever the install runs. In a monorepo that is the workspace root with the shared node_modules, so the decision is global: a block in the root package.json wins, and when the root has none the postinstall reads the workspace members (npm/yarn workspaces or pnpm-workspace.yaml) that depend on this package and skips an asset only when every one of them opts out. The build flag is read from the app's package.json (the one beside ios/ and android/), so each app in a monorepo enables or disables features independently.

You can also narrow or disable a feature purely at build time (the assets stay downloaded but are never compiled) - for example trimming the code-highlighting language set. See Code-block highlighting and LaTeX math for the per-feature options.

Expo works with the same block - node_modules survives npx expo prebuild, so no config plugin is needed (a dedicated plugin was removed; see Breaking changes).

caution

Because it is a compile/link-time decision, it cannot be changed in Expo Go.

Optional native features​

The same enriched-markdown block also gates features that ship no downloaded assets - they are compiled from source already in the package, so they are a pure build-time choice rather than part of the install-time download above.

{
"enriched-markdown": {
"enableVideo": false
}
}

enableVideo (default true) controls the native video players used to render the <video> element. Turning it off drops AVPlayerViewController on iOS and the ExoPlayer dependency on Android - worth roughly 2-3 MB of Android binary. With it off a <video /> tag renders nothing and markdownStyle.video has no effect.

It is read from the app's package.json and applies the same way as the flags above: run pod install plus a clean build on iOS after changing it, and it cannot be changed in Expo Go.