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.
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
- Expo
- Bare React Native
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.
Point react-native.config.js at your fonts folder:
// react-native.config.js
module.exports = {
assets: ['./assets/fonts'],
};
Copy the fonts into the native projects with
react-native-asset, then
rebuild:
npx react-native-asset
On iOS this registers the fonts in Info.plist (UIAppFonts); on Android it
copies them into android/app/src/main/assets/fonts. Rebuild the app afterwards.
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.
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).
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.
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 = ``;
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
| Asset | Source | Used by |
|---|---|---|
| tree-sitter runtime | GitHub release tarball | code highlighting (iOS + Android) |
grammar sources (parser.c / scanner.c / highlights.scm) | npm registry | code highlighting (iOS + Android) |
| RaTeX XCFramework + fonts | GitHub release | LaTeX 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.organdgithub.com. tar(present on macOS, Linux, and Windows 10+). RaTeX also needsunzip, 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-markdownentry): 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
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.
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).
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.