Skip to main content

EnrichedMarkdownTextInput

EnrichedMarkdownTextInput is a native, WYSIWYG rich text editor whose content is Markdown: the user sees formatted text as they type - bold looks bold, not **bold** - while the value is stored as Markdown. Seed it with a Markdown string and read the edited content back as Markdown whenever you need it. Markdown is the source of truth on both sides of the edit, so content round-trips cleanly in and out. It renders with the platform's native text stack (UITextView on iOS, EditText on Android), and is an uncontrolled input - it does not hold its value in React state or props, but talks directly to the underlying native component, which keeps it fast and simple.

import { useRef, useState } from 'react';
import { View, Button, StyleSheet } from 'react-native';
import {
EnrichedMarkdownTextInput,
type EnrichedMarkdownTextInputInstance,
type StyleState,
} from 'react-native-enriched-markdown';

export default function App() {
const ref = useRef<EnrichedMarkdownTextInputInstance>(null);
const [state, setState] = useState<StyleState | null>(null);

return (
<View style={styles.container}>
<EnrichedMarkdownTextInput
ref={ref}
placeholder="Type here..."
onChangeState={setState}
style={styles.input}
/>
<Button
title={state?.bold.isActive ? 'Unbold' : 'Bold'}
color={state?.bold.isActive ? 'green' : 'gray'}
onPress={() => ref.current?.toggleBold()}
/>
</View>
);
}

const styles = StyleSheet.create({
container: { gap: 8 },
input: {
fontSize: 18,
padding: 12,
minHeight: 96,
backgroundColor: '#eef0ff',
},
});

The example above shows the two patterns you will use everywhere:

  • Drive formatting through the ref. Call methods like toggleBold() to apply a style to the current selection or cursor.
  • Read active styles from onChangeState. It reports which styles are active at the cursor, so your toolbar can highlight the right buttons.

Props​

EnrichedMarkdownTextInput accepts every prop below. It also forwards the standard React Native View props - such as testID, onLayout, pointerEvents, and the accessibility* props - to the underlying native view.

note

Props marked with a iOS only, Android only or Web only badge only take effect on that platform - they are flagged with a matching colored dot in the table of contents. The input also runs on web as an ExperimentalExperimental: this feature is still taking shape and may get breaking changes, including in nightly releases. editor that supports a subset of this page - see Web editor for which props and methods apply there. Examples for what the web editor supports run live below; the rest show their source.

Every shape printed inline below is an exported type you can import from the package root: MarkdownTextInputStyle, StyleState, HeadingLevel, ContextMenuItem, InputSelectionMenuConfig, FormatMenuConfig, MarkdownShortcutsConfig, CaretRect, and the event payloads. Note that the unprefixed ContextMenuItem is this component's type; the renderer's equivalent is exported as TextContextMenuItem.

defaultValue​

Initial Markdown content. It is parsed and formatting is applied on mount.

TypeDefault
string—
important

Following React Native TextInput semantics, defaultValue is read once, at mount - later changes to it are ignored. To replace the content imperatively afterwards, call setValue on the ref.

Loading…

placeholder​

Text shown while the input is empty.

TypeDefault
string—
important

The placeholder is plain text, not Markdown. Unlike defaultValue, Markdown syntax here is shown literally - **bold** appears as those raw characters, never as bold text. It is drawn as a plain label, so it also ignores markdownStyle; use placeholderTextColor and the base style font to style it.

Loading…

placeholderTextColor​

Color of the placeholder text.

TypeDefault
ColorValue—
Loading…

editable​

Whether the input can be edited. On iOS and macOS, false leaves the content selectable but unchangeable. On Android it maps to the view's enabled state, which disables the whole text field - no touch, no selection, no focus.

TypeDefault
booleantrue
Loading…

autoFocus​

Whether the input focuses itself on mount.

TypeDefault
booleanfalse
import { EnrichedMarkdownTextInput } from 'react-native-enriched-markdown';

export default function App() {
return <EnrichedMarkdownTextInput autoFocus placeholder="Focused on mount" />;
}

scrollEnabled​

Whether the input scrolls its own content when it overflows. Set to false when the input lives inside an outer ScrollView and you size it to its content; pair it with onCaretRectChange to keep the caret visible.

TypeDefault
booleantrue
import { ScrollView } from 'react-native';
import { EnrichedMarkdownTextInput } from 'react-native-enriched-markdown';

// With scrollEnabled={false} the input grows to fit its content, letting an
// outer ScrollView own the scrolling instead. Pair it with onCaretRectChange
// to keep the caret visible.
export default function App() {
return (
<ScrollView keyboardShouldPersistTaps="handled">
<EnrichedMarkdownTextInput scrollEnabled={false} placeholder="Type a lot..." />
</ScrollView>
);
}

multiline​

Whether the input accepts multiple lines.

TypeDefault
booleantrue
import { EnrichedMarkdownTextInput } from 'react-native-enriched-markdown';

export default function App() {
return (
<EnrichedMarkdownTextInput multiline={false} placeholder="Single line" />
);
}

markdownShortcuts​

Converts Markdown block prefixes typed at the start of a paragraph into real blocks, the way Notion, Bear and Obsidian do. Typing ## turns the line into a heading, - into a bullet item, 1. into a numbered item - and the prefix plus its space is removed from the text.

true enables all three. Pass a MarkdownShortcutsConfig to enable a subset; an omitted key is off, so { heading: true } enables headings and nothing else.

TypeDefault
boolean | MarkdownShortcutsConfigfalse
interface MarkdownShortcutsConfig {
heading?: boolean; // `#` through `######` + space
unorderedList?: boolean; // `-`, `*` or `+` + space
orderedList?: boolean; // `1.` or `1)` + space
}
note

It is off by default because it is a user-visible behavior change: an app where # starts a tag, or - is just a dash, would otherwise see those characters disappear as the user types.

import { EnrichedMarkdownTextInput } from 'react-native-enriched-markdown';

export default function App() {
return (
<EnrichedMarkdownTextInput
placeholder="Type '## ' or '- ' at the start of a line"
// `true` would enable all three; this enables only headings and bullets,
// leaving a typed "1. " as literal text.
markdownShortcuts={{ heading: true, unorderedList: true }}
/>
);
}

autoCapitalize​

Auto-capitalization behavior, mirroring the prop of the same name on React Native's TextInput.

TypeDefault
'none' | 'sentences' | 'words' | 'characters''sentences'
note

The prop is typed as string so that future platform values do not break the type. Only the four values above are recognized; anything else silently falls back to 'none' on Android.

import { EnrichedMarkdownTextInput } from 'react-native-enriched-markdown';

export default function App() {
return (
<EnrichedMarkdownTextInput
autoCapitalize="words"
placeholder="Each Word Is Capitalized"
/>
);
}

cursorColor​

Color of the text caret.

TypeDefault
ColorValue—
caution

On Android and macOS, cursorColor (the caret) and selectionColor (the selection highlight) are independent - set them to whatever two colors you like.

On iOS there is no separate caret color: both props map to the text view's single tintColor, which UIKit uses for the caret, selection, and handles together. If you set them to two different colors, selectionColor wins (it is applied last) - the caret, selection, and handles all take the selectionColor, and cursorColor has no visible effect. So on iOS, style the caret through selectionColor and leave cursorColor unset.

Loading…

selectionColor​

Color of the text selection highlight. On Android and macOS it affects only the selection background. On iOS it shares a single tint with the caret and selection handles, so it also sets the caret color and takes precedence over cursorColor.

TypeDefault
ColorValue—
Loading…

style​

Style for the input view. Accepts both ViewStyle layout properties (padding, margin, backgroundColor, the flexbox props) and TextStyle properties (fontSize, color, fontFamily, lineHeight), which set the base text appearance the Markdown styles build on.

TypeDefault
ViewStyle | TextStyle—
note

The input uses the regular style prop directly, just like a native TextInput: here TextStyle values (fontSize, color, ...) really do set the base text appearance, so the name is honest. The read-only EnrichedMarkdownText renames its equivalent to containerStyle instead, because there style would only affect the wrapper box, not the rendered text. To restyle the formatted elements (bold, links, headings, ...), use markdownStyle.

Loading…

markdownStyle​

Overrides for how formatted text is painted inside the input. Its type, MarkdownTextInputStyle, is a subset of the renderer's MarkdownStyle - the editor styles inline formatting, links, spoilers, headings, and list item spacing. Every field is optional and falls back to a default that matches the read-only EnrichedMarkdownText renderer, so content looks the same in the editor and a rendered preview. See Editor styles for the full list of styleable properties.

TypeDefault
MarkdownTextInputStyle{}

On web, the experimental web editor honors every key except linkVariants.

Loading…

linkRegex​

A custom regular expression for auto-link detection: the input tests typed text against it and turns matches into Markdown links. Pass null to disable auto-link detection entirely, or omit the prop to use the built-in URL matcher.

TypeDefault
RegExp | nullbuilt-in URL matcher

By default, URLs like google.com, www.google.com, and https://google.com are detected when followed by a space or newline. Bare domains and www. prefixes are normalized with https:// (e.g. google.com becomes [google.com](https://google.com)).

note

When a manual link (setLink / insertLink) overlaps an auto-detected one, the manual link wins. Auto-link detection skips ranges that already contain a manual link.

import { EnrichedMarkdownTextInput } from 'react-native-enriched-markdown';

// Only turn fully-qualified http(s) URLs into links; pass null to disable.
export default function App() {
return (
<EnrichedMarkdownTextInput
linkRegex={/https?:\/\/[^\s]+/i}
placeholder="Type a URL like https://swmansion.com"
/>
);
}

mentionIndicators​

Trigger characters that start a mention flow (e.g. ['@', '#']). Typing one begins a mention; the mention events and insertMention drive the suggestion UI.

TypeDefault
string[][]
note

See the Mentions guide for the full setup: events, ref methods, and per-pattern styling with markdownStyle.linkVariants.

import { useRef, useState } from 'react';
import { View, Text, Pressable } from 'react-native';
import {
EnrichedMarkdownTextInput,
type EnrichedMarkdownTextInputInstance,
} from 'react-native-enriched-markdown';

const USERS = ['alice', 'bob', 'carol'];

export default function App() {
const ref = useRef<EnrichedMarkdownTextInputInstance>(null);
const [query, setQuery] = useState<string | null>(null);

const matches =
query != null
? USERS.filter((u) => u.startsWith(query.toLowerCase()))
: [];

return (
<View style={{ gap: 8 }}>
<EnrichedMarkdownTextInput
ref={ref}
mentionIndicators={['@']}
placeholder="Mention someone with @"
onStartMention={() => setQuery('')}
onChangeMention={({ text }) => setQuery(text)}
onEndMention={() => setQuery(null)}
/>
{matches.map((user) => (
<Pressable
key={user}
onPress={() =>
ref.current?.insertMention(`@${user}`, `https://app/users/${user}`)
}
>
<Text>@{user}</Text>
</Pressable>
))}
</View>
);
}

contextMenuItems​

Custom items added to the text selection context menu. They appear before the system actions and are hidden when visible: false. Available on iOS, Android, and macOS (iOS requires 16+; earlier versions ignore the prop).

TypeDefault
ContextMenuItem[]—
interface ContextMenuItem {
/** Label shown in the context menu. */
text: string;
/** SF Symbol name for the item icon (iOS/macOS only; ignored on Android). */
icon?: string;
onPress: (event: {
/** The selected text at the time of the press. */
text: string;
/** Absolute character range of the selection within the full content. */
selection: { start: number; end: number };
/** Active formatting styles at the time of the press. */
styleState: StyleState;
}) => void;
/** When false, the item is not shown. Defaults to true. */
visible?: boolean;
}
import { EnrichedMarkdownTextInput } from 'react-native-enriched-markdown';

export default function App() {
return (
<EnrichedMarkdownTextInput
defaultValue="Select this text, then open the menu."
contextMenuItems={[
{
text: 'Summarize with AI',
icon: 'sparkles',
onPress: ({ text, styleState }) => {
console.log('Selected:', text, 'bold?', styleState.bold.isActive);
},
},
]}
/>
);
}

selectionMenuConfig​

Controls the built-in items in the text selection menu - the Format submenu and the Copy as Markdown action - and lets you localize their labels. Custom app actions are controlled separately with contextMenuItems. Each item takes { enabled, label }: enabled toggles visibility and label overrides the English default. Available on iOS, Android, and macOS (iOS requires 16+; earlier versions ignore the prop).

TypeDefault
InputSelectionMenuConfig{}
interface InputSelectionMenuConfig {
/** The "Format" submenu. @default { enabled: true, label: "Format" } */
format?: { enabled?: boolean; label?: string };
/** "Copy as Markdown" action. @default { enabled: true, label: "Copy as Markdown" } */
copyAsMarkdown?: { enabled?: boolean; label?: string };
}
note

System Cut / Copy / Paste / Select All come from the platform and are already localized by the device language - they are not exposed through this prop.

import { EnrichedMarkdownTextInput } from 'react-native-enriched-markdown';

export default function App() {
return (
<EnrichedMarkdownTextInput
defaultValue="Select text to see the tailored menu."
selectionMenuConfig={{
// Hide the Copy as Markdown action and relabel the Format submenu.
copyAsMarkdown: { enabled: false },
format: { label: 'Formatting' },
}}
/>
);
}

formatMenuConfig​

Controls which items appear inside the Format submenu, and each item's label. Only effective while selectionMenuConfig.format is enabled (the default). Same { enabled, label } shape as above. Available on iOS, Android, and macOS (iOS requires 16+; earlier versions ignore the prop).

TypeDefault
FormatMenuConfig{}
interface FormatMenuConfig {
/** @default { enabled: true, label: "Bold" } */
bold?: { enabled?: boolean; label?: string };
/** @default { enabled: true, label: "Italic" } */
italic?: { enabled?: boolean; label?: string };
/** @default { enabled: true, label: "Underline" } */
underline?: { enabled?: boolean; label?: string };
/** @default { enabled: true, label: "Strikethrough" } */
strikethrough?: { enabled?: boolean; label?: string };
/** @default { enabled: true, label: "Spoiler" } */
spoiler?: { enabled?: boolean; label?: string };
/** @default { enabled: true, label: "Link" } */
link?: { enabled?: boolean; label?: string };
}
import { EnrichedMarkdownTextInput } from 'react-native-enriched-markdown';

export default function App() {
return (
<EnrichedMarkdownTextInput
defaultValue="Select text, open Format, and note the trimmed list."
formatMenuConfig={{
// Keep only Bold, Italic, and Link in the Format submenu.
underline: { enabled: false },
strikethrough: { enabled: false },
spoiler: { enabled: false },
}}
/>
);
}

writingDirection iOS only​

Paragraph writing direction. iOS only.

important

Android's EditText resolves direction per paragraph via the platform Bidi heuristic (TEXT_DIRECTION_FIRST_STRONG) and is unaffected by this prop.

TypeDefault
'auto' | 'ltr' | 'rtl' | 'first-strong''first-strong'
  • 'first-strong' (default): library extension. Each paragraph resolves its base direction from its first strong directional character, so mixed Arabic/Hebrew/English content right-aligns correctly as you type. Neutral-only paragraphs fall back to the view's resolved layout direction. Mirrors Android.
  • 'auto': React Native parity. TextKit follows the app's userInterfaceLayoutDirection; mixed-direction paragraphs do not auto-resolve.
  • 'ltr' / 'rtl': force the base direction on every paragraph.
note

The placeholder follows the host view's layout direction, not this prop. For an RTL placeholder, wrap the input in <View style={{ direction: 'rtl' }}> or set I18nManager.forceRTL(true). See RTL support for the full behavior.

import { EnrichedMarkdownTextInput } from 'react-native-enriched-markdown';

export default function App() {
return (
<EnrichedMarkdownTextInput writingDirection="rtl" placeholder="اكتب هنا..." />
);
}

hitSlop​

Extends the touchable area around the input for the press handlers, so a tap that lands just outside the view still focuses it. Same as hitSlop on React Native View and TextInput.

TypeDefault
ViewProps['hitSlop']—

rejectResponderTermination iOS only​

When true, an ancestor ScrollView or Pressable cannot take the touch responder away from the input mid-press. Same as rejectResponderTermination on React Native TextInput.

TypeDefault
booleantrue

Events​

Every event below is an optional callback prop. Content-reading events fire whenever the relevant part of the input changes.

onChangeText​

Fires when the plain text changes. Returns the text without Markdown syntax.

TypeDefault
(text: string) => void—
Loading…

onChangeMarkdown​

Fires when the Markdown representation changes. Returns the full Markdown string.

TypeDefault
(markdown: string) => void—
note

Producing the Markdown means rebuilding the whole document into a Markdown string on every change, which is not free. Unlike the other events - cheap byproducts of editing that always fire - this serialization runs only when you pass the callback, so omit it unless you actually need the Markdown live.

Loading…

onChangeSelection​

Fires when the selection changes. Useful when applying links to a range.

TypeDefault
(selection: { start: number; end: number }) => void—
Loading…

onChangeState​

Fires when the active style state changes. Each style reports isActive; heading additionally carries the cursor paragraph's level, and the list styles their nesting depth. level is a HeadingLevel (1 to 6) and is only meaningful while heading.isActive is true - it is never 0, so test isActive rather than comparing the level.

TypeDefault
(state: StyleState) => void—
interface StyleState {
bold: { isActive: boolean };
italic: { isActive: boolean };
underline: { isActive: boolean };
strikethrough: { isActive: boolean };
spoiler: { isActive: boolean };
link: { isActive: boolean };
// Heading level of the cursor's paragraph. Read `isActive` to find out
// whether the paragraph is a heading at all - `level` is never 0.
heading: { isActive: boolean; level: HeadingLevel }; // 1 | 2 | 3 | 4 | 5 | 6
// `depth` is the 0-based nesting level and is only meaningful while
// `isActive` is true (it is 0 when the cursor is not in a list).
unorderedList: { isActive: boolean; depth: number };
orderedList: { isActive: boolean; depth: number };
}
Loading…

onChangeHistoryState Web only​

Fires when whether undo() or redo() has a step to take changes. It fires only when one of the two values flips, not on every edit, so it is cheap to drive toolbar buttons from. Part of the experimental web editor; iOS and Android never call it.

TypeDefault
(state: { canUndo: boolean; canRedo: boolean }) => void—

onKeyPress​

Fires on every keystroke, before the change is applied - mirroring React Native TextInput's onKeyPress. nativeEvent.key is the pressed character or a named key: Backspace, Enter, Tab (iOS additionally reports Escape).

TypeDefault
(e: NativeSyntheticEvent<OnKeyPressEvent>) => void—
interface OnKeyPressEvent {
key: string; // the pressed character, or a named key: Backspace, Enter, Tab, Escape (iOS)
}

This is the one callback on this page that hands you the raw NativeSyntheticEvent, so read the key from e.nativeEvent.key; every other callback is unwrapped in JS and gives you a plain value.

note

On Android, the key reported for soft-keyboard input can lag actual typing when autocomplete suggestions are involved. Paste operations do not fire this event on any platform - paste is intercepted before the delegate that emits the key.

import { EnrichedMarkdownTextInput } from 'react-native-enriched-markdown';

export default function App() {
return (
<EnrichedMarkdownTextInput
placeholder="Type to log keys"
onKeyPress={({ nativeEvent: { key } }) => {
console.log('Pressed key:', key);
}}
/>
);
}

onCaretRectChange​

Fires when the caret's pixel position changes (typing, selection change, content reflow). The rect is relative to the input's top-left corner, in density-independent pixels. The native side diffs the rect before emitting, so redundant events are suppressed. For one-off queries, use getCaretRect instead.

TypeDefault
(rect: CaretRect) => void—
interface CaretRect {
x: number;
y: number;
width: number;
height: number;
}
import { useState } from 'react';
import { View, Text } from 'react-native';
import {
EnrichedMarkdownTextInput,
type CaretRect,
} from 'react-native-enriched-markdown';

export default function App() {
const [rect, setRect] = useState<CaretRect | null>(null);

return (
<View style={{ gap: 8 }}>
<EnrichedMarkdownTextInput
scrollEnabled={false}
placeholder="Move the caret..."
onCaretRectChange={setRect}
/>
{rect && (
<Text>
Caret at x:{Math.round(rect.x)} y:{Math.round(rect.y)}
</Text>
)}
</View>
);
}

onLinkDetected​

Fires when auto-link detection turns typed text into a link. Fires only for newly detected links, not for ones that already existed and stayed unchanged.

TypeDefault
(event: { text: string; url: string; start: number; end: number }) => void—
import { EnrichedMarkdownTextInput } from 'react-native-enriched-markdown';

export default function App() {
return (
<EnrichedMarkdownTextInput
placeholder="Type a URL, e.g. swmansion.com"
onLinkDetected={({ text, url, start, end }) => {
console.log(`Detected ${text} -> ${url} at [${start}, ${end}]`);
}}
/>
);
}

onStartMention​

Fires when a new mention flow starts (an indicator from mentionIndicators is typed).

TypeDefault
(event: { indicator: string }) => void—
note

onStartMention, onChangeMention, and onEndMention are the events of the mention flow; the example below shows all three together. For general information about mentions - trigger setup, the suggestion UI, ref methods, and per-pattern styling - see the Mentions guide.

import { useRef, useState } from 'react';
import { View, Text, Pressable } from 'react-native';
import {
EnrichedMarkdownTextInput,
type EnrichedMarkdownTextInputInstance,
} from 'react-native-enriched-markdown';

const USERS = ['alice', 'bob', 'carol'];

export default function App() {
const ref = useRef<EnrichedMarkdownTextInputInstance>(null);
const [query, setQuery] = useState<string | null>(null);

const matches =
query != null
? USERS.filter((u) => u.startsWith(query.toLowerCase()))
: [];

return (
<View style={{ gap: 8 }}>
<EnrichedMarkdownTextInput
ref={ref}
mentionIndicators={['@']}
placeholder="Mention someone with @"
onStartMention={() => setQuery('')}
onChangeMention={({ text }) => setQuery(text)}
onEndMention={() => setQuery(null)}
/>
{matches.map((user) => (
<Pressable
key={user}
onPress={() =>
ref.current?.insertMention(`@${user}`, `https://app/users/${user}`)
}
>
<Text>@{user}</Text>
</Pressable>
))}
</View>
);
}

onChangeMention​

Fires on every keystroke while a mention flow is active - use the running text to filter your suggestion list.

TypeDefault
(event: { indicator: string; text: string }) => void—

Part of the same flow - see the mention example under onStartMention.

onEndMention​

Fires when the active mention flow ends (a selection is inserted, or the token is dismissed).

TypeDefault
(event: { indicator: string }) => void—

Part of the same flow - see the mention example under onStartMention.

onFocus​

Fires when the input gains focus.

TypeDefault
() => void—
Loading…

onBlur​

Fires when the input loses focus.

TypeDefault
() => void—

The onFocus example wires up both callbacks.

note

The input registers with React Native's text-input focus tracking (TextInput.State), so blur also happens through the platform's standard keyboard-dismiss paths: taps outside the input inside a ScrollView (per its keyboardShouldPersistTaps) and Keyboard.dismiss().

onPress, onPressIn, onPressOut​

Fire for taps on the input, like the same props on React Native TextInput. The input claims the JS touch responder, so a tap that focuses it does not also fire the onPress of an ancestor Pressable. After onPress runs the input focuses itself - unless editable is false - without moving the selection.

TypeDefault
(event: GestureResponderEvent) => unknown—

Use these three, together with hitSlop and rejectResponderTermination, rather than the raw responder props: press handling overrides the onStartShouldSetResponder / onResponderGrant family that the forwarded View props would otherwise give you, exactly as it does on TextInput.

import { useState } from 'react';
import { Text, Pressable, StyleSheet } from 'react-native';
import { EnrichedMarkdownTextInput } from 'react-native-enriched-markdown';

export default function App() {
const [log, setLog] = useState('Tap the input, or the card around it');

return (
<Pressable
style={styles.card}
onPress={() => setLog('Card pressed - the tap missed the input')}
>
<EnrichedMarkdownTextInput
placeholder="Tap me"
hitSlop={12}
onPressIn={() => setLog('Input press in')}
onPressOut={() => setLog('Input press out')}
onPress={() => setLog('Input pressed - the card stayed quiet')}
style={styles.input}
/>
<Text>{log}</Text>
</Pressable>
);
}

const styles = StyleSheet.create({
card: { gap: 8, padding: 16, backgroundColor: '#f6f6f8' },
input: {
fontSize: 18,
padding: 12,
minHeight: 80,
backgroundColor: '#eef0ff',
},
});

Ref methods​

All methods are called imperatively on a ref typed as EnrichedMarkdownTextInputInstance:

const ref = useRef<EnrichedMarkdownTextInputInstance>(null);
// ...
ref.current?.toggleBold();

The instance also exposes the standard React Native host measurement methods (measure, measureInWindow, measureLayout).

note

The inline toggle* methods act on the current selection, or - when nothing is selected - queue the style so it applies to the next characters typed. The block methods (toggleHeading, list methods) act on the whole paragraph the cursor touches.

focus()​

Focuses the input.

Loading…

blur()​

Blurs the input. The focus() example above wires up both methods.

setValue(markdown: string)​

Replaces the entire content by parsing markdown and applying its formatting. This is how you set content after mount (defaultValue is only read once).

Loading…

getMarkdown(): Promise<string>​

Returns the input's current content as a Markdown string. The result is a promise because the content is read from the native side.

Loading…

insertText(text: string)​

Parses text as Markdown and inserts it at the cursor, replacing the selection if there is one. Leading and trailing newlines are preserved, so wrap block content in newlines to keep it on its own lines - insertText('\n- item\n') in the middle of test yields te, a - item bullet, and st on separate lines. An empty string is a no-op.

Loading…

copyToClipboard()​

Copies the full content to the system clipboard, as if the user selected all and pressed Copy. The selection is left unchanged, and calling it on an empty input is a no-op.

note

Both platforms write a dual representation: clean plain text for external paste targets, plus the Markdown source under a private type (an extra pasteboard type on iOS and macOS, a vendor MIME type on Android). Pasting back into an EnrichedMarkdownTextInput therefore restores the formatting on either platform, while external apps receive plain text.

import { useRef } from 'react';
import { View, Button, StyleSheet } from 'react-native';
import {
EnrichedMarkdownTextInput,
type EnrichedMarkdownTextInputInstance,
} from 'react-native-enriched-markdown';

export default function App() {
const ref = useRef<EnrichedMarkdownTextInputInstance>(null);

return (
<View style={styles.container}>
<EnrichedMarkdownTextInput
ref={ref}
defaultValue="This whole document copies to the clipboard, formatting included."
style={styles.input}
/>
<Button title="Copy all" onPress={() => ref.current?.copyToClipboard()} />
</View>
);
}

const styles = StyleSheet.create({
container: { gap: 8 },
input: { fontSize: 18, padding: 12, minHeight: 96, backgroundColor: '#eef0ff' },
});

setSelection(start: number, end: number)​

Sets the selection range.

Loading…

getCaretRect(): Promise<CaretRect>​

Resolves with the caret's current pixel position relative to the input. For continuous tracking, prefer onCaretRectChange.

Loading…

toggleBold()​

Toggles bold on the current selection or cursor.

Loading…

toggleItalic()​

Toggles italic on the current selection or cursor.

Loading…

toggleUnderline()​

Toggles underline on the current selection or cursor.

Loading…

toggleStrikethrough()​

Toggles strikethrough on the current selection or cursor.

Loading…

toggleSpoiler()​

Toggles spoiler on the current selection or cursor.

Loading…

toggleHeading(level: HeadingLevel)​

Toggles a heading of the given level (1-6, the HeadingLevel union) on the cursor's paragraph. Calling it with the level already applied turns the paragraph back into regular text.

note

Headings are single-line blocks. An emptied heading line stays a heading until you toggle it off, and pressing Enter at the end of a heading starts a regular paragraph (headings do not continue like list items).

Loading…

toggleUnorderedList()​

Turns every paragraph the selection touches into bullet items, or back into regular paragraphs. Toggling one list type onto a line carrying the other replaces it, keeping the nesting depth.

Loading…

toggleOrderedList()​

Same as above for numbered items. Numbering derives from each item's position among its adjacent same-depth siblings, so it stays correct as items are added, removed, or reordered.

Loading…

indentList()​

Nests the current item one level deeper, up to a maximum depth of 5. On a non-list paragraph it starts a bullet list. Equivalent to Tab on a hardware keyboard.

Loading…

outdentList()​

Lifts the current item out one level. Outdenting a depth-0 item removes the bullet. Equivalent to Shift+Tab. The indentList() example above includes an outdent button.

note

Pressing Return inside an item starts a new item at the same depth; pressing it on an empty item exits the list. Backspace at the start of an item outdents it. List items are single-line: Markdown imported with multi-paragraph (loose) items keeps only each item's first line as a list item.

undo() Web only​

Steps back over the last edit: typing up to a pause or a caret move, a run of Backspaces, a paste, a cut, a newline, an IME composition, or one formatting command. Text, formatting and selection all return to how they were before it. Part of the experimental web editor, where Cmd+Z / Ctrl+Z and the browser's Edit menu do the same; a no-op on iOS and Android.

redo() Web only​

Re-applies the last step undo() took back. Any new edit clears what is left to redo. Part of the experimental web editor, where Cmd+Shift+Z / Ctrl+Shift+Z / Ctrl+Y and the browser's Edit menu do the same; a no-op on iOS and Android.

setLink(url: string)​

Applies a link URL to the currently selected text.

import { useRef } from 'react';
import { View, Button, StyleSheet } from 'react-native';
import {
EnrichedMarkdownTextInput,
type EnrichedMarkdownTextInputInstance,
} from 'react-native-enriched-markdown';

export default function App() {
const ref = useRef<EnrichedMarkdownTextInputInstance>(null);

return (
<View style={styles.container}>
<EnrichedMarkdownTextInput
ref={ref}
defaultValue="Select this text, then turn it into a link."
style={styles.input}
/>
<Button
title="Link selection"
onPress={() => ref.current?.setLink('https://swmansion.com')}
/>
</View>
);
}

const styles = StyleSheet.create({
container: { gap: 8 },
input: { fontSize: 18, padding: 12, minHeight: 96, backgroundColor: '#eef0ff' },
});

insertLink(text: string, url: string)​

Inserts a new link with the given text and URL at the cursor. Useful when there is no selection.

import { useRef } from 'react';
import { View, Button, StyleSheet } from 'react-native';
import {
EnrichedMarkdownTextInput,
type EnrichedMarkdownTextInputInstance,
} from 'react-native-enriched-markdown';

// insertLink adds a brand-new link at the cursor - handy when nothing is selected.
export default function App() {
const ref = useRef<EnrichedMarkdownTextInputInstance>(null);

return (
<View style={styles.container}>
<EnrichedMarkdownTextInput
ref={ref}
defaultValue="Place the cursor here: "
style={styles.input}
/>
<Button
title="Insert link"
onPress={() =>
ref.current?.insertLink('Software Mansion', 'https://swmansion.com')
}
/>
</View>
);
}

const styles = StyleSheet.create({
container: { gap: 8 },
input: { fontSize: 18, padding: 12, minHeight: 96, backgroundColor: '#eef0ff' },
});

Removes the link from the current selection.

import { useRef } from 'react';
import { View, Button, StyleSheet } from 'react-native';
import {
EnrichedMarkdownTextInput,
type EnrichedMarkdownTextInputInstance,
} from 'react-native-enriched-markdown';

export default function App() {
const ref = useRef<EnrichedMarkdownTextInputInstance>(null);

return (
<View style={styles.container}>
<EnrichedMarkdownTextInput
ref={ref}
defaultValue="Put the cursor inside this [link](https://swmansion.com), then remove it."
style={styles.input}
/>
<Button title="Remove link" onPress={() => ref.current?.removeLink()} />
</View>
);
}

const styles = StyleSheet.create({
container: { gap: 8 },
input: { fontSize: 18, padding: 12, minHeight: 96, backgroundColor: '#eef0ff' },
});

startMention(indicator: string)​

Programmatically starts a mention flow by inserting indicator at the cursor. The indicator must be listed in mentionIndicators. Useful for toolbar buttons.

import { useRef, useState } from 'react';
import { View, Button, Text, StyleSheet } from 'react-native';
import {
EnrichedMarkdownTextInput,
type EnrichedMarkdownTextInputInstance,
} from 'react-native-enriched-markdown';

// startMention inserts the indicator and opens a mention flow from a button,
// instead of waiting for the user to type "@".
export default function App() {
const ref = useRef<EnrichedMarkdownTextInputInstance>(null);
const [active, setActive] = useState(false);

return (
<View style={styles.container}>
<EnrichedMarkdownTextInput
ref={ref}
mentionIndicators={['@']}
placeholder="Tap the button to start a mention"
onStartMention={() => setActive(true)}
onEndMention={() => setActive(false)}
style={styles.input}
/>
<Button title="Mention someone" onPress={() => ref.current?.startMention('@')} />
<Text>{active ? 'Mention flow active' : 'Idle'}</Text>
</View>
);
}

const styles = StyleSheet.create({
container: { gap: 8 },
input: { fontSize: 18, padding: 12, minHeight: 96, backgroundColor: '#eef0ff' },
});

insertMention(displayText: string, url: string)​

Replaces the active mention token with a formatted link, serialized as [displayText](url). It is a no-op unless a mention flow is active - for a "recent mentions" button outside a flow, use insertLink instead. A trailing space is appended unless the next character is already whitespace, and the caret is parked after it.

import { useRef, useState } from 'react';
import { View, Text, Pressable } from 'react-native';
import {
EnrichedMarkdownTextInput,
type EnrichedMarkdownTextInputInstance,
} from 'react-native-enriched-markdown';

const USERS = ['alice', 'bob', 'carol'];

export default function App() {
const ref = useRef<EnrichedMarkdownTextInputInstance>(null);
const [query, setQuery] = useState<string | null>(null);

const matches =
query != null
? USERS.filter((u) => u.startsWith(query.toLowerCase()))
: [];

return (
<View style={{ gap: 8 }}>
<EnrichedMarkdownTextInput
ref={ref}
mentionIndicators={['@']}
placeholder="Mention someone with @"
onStartMention={() => setQuery('')}
onChangeMention={({ text }) => setQuery(text)}
onEndMention={() => setQuery(null)}
/>
{matches.map((user) => (
<Pressable
key={user}
onPress={() =>
ref.current?.insertMention(`@${user}`, `https://app/users/${user}`)
}
>
<Text>@{user}</Text>
</Pressable>
))}
</View>
);
}

See also​

  • Mentions - trigger indicators, suggestion events, and per-pattern link styling.
  • EnrichedMarkdownText - the read-only renderer that pairs with this input.
  • Style properties - all styleable properties, including a dark-mode recipe.
  • Testing with Jest - the shipped Jest mock for asserting on input, ref methods, and emitted Markdown.
  • RTL support - right-to-left languages and per-element RTL behavior.