Skip to main content

Element structure

This page is the catalog of every Markdown element react-native-enriched-markdown renders: what exists, whether it is a block or an inline element, and how elements nest. It is the map that ties the rest of the reference together:

  • For the syntax of each element (how you write it), see Core concepts.
  • For styling each element through markdownStyle, see Style properties.
  • For which targets support each element (React Native and Web), see Feature support.

The syntax and style-property columns below are pointers for scanning - follow the links above for the full detail.

Block vs. inline elements​

Every element falls into one of two categories, and the distinction drives how it lays out and how it inherits typography:

  • Block elements are structural containers. They occupy whole lines, stack vertically, and each establishes its own typography context (font size, line height, spacing).
  • Inline elements live within a block and style a run of text. They inherit the containing block's typography and layer additional styling on top - bold text inside a heading takes the heading's size and adds weight.

Supported elements​

Block elements​

ElementSyntaxStyle propertyDescription
Headings# H1 to
###### H6
h1 - h6Six levels of headings
ParagraphsPlain textparagraphDefault text container
Blockquotes> QuoteblockquoteQuoted content with an accent bar, unlimited nesting
Admonitions> [!NOTE]blockquote.admonitionsGitHub alert callouts with an icon and title header (requires flavor="github")
Code blocks``` code ```codeBlockMulti-line code containers; with flavor="github" rendered as a block component with a language header and copy-code button
Unordered lists- Item,
* Item, or
+ Item
listBullet lists with unlimited nesting
Ordered lists1. ItemlistNumbered lists with unlimited nesting
Task lists- [x] Done,
- [ ] Todo
taskListInteractive checkboxes (requires flavor="github"). There is no separate task-list node - a task item is a list item carrying isTask and taskChecked
Thematic breaks---, ***,
or ___
thematicBreakHorizontal rule separator
Images![alt](url)imageBlock-level images with spacing
Videos<video src="url" />videoNative video player (requires flavor="github")
Tables| col | col |tableGFM tables with alignment support (requires flavor="github")
Math blocks$$...$$mathBlock-level LaTeX math (display equations) (requires flavor="github" and md4cFlags={{ latexMath: true }}, the latter on by default)

Inline elements​

Inline elements inherit the typography of their parent block and add their own styling on top. For how inheritance resolves and the base block style, see Style inheritance.

ElementSyntaxStyle propertyAdds
Bold**text** (or __text__)strongBold weight, optional color
Italic*text* (or _text_)emItalic style, optional color
Underline_text_underlineUnderline; custom underline color on iOS and web, Android always underlines in the text color (requires md4cFlags={{ underline: true }})
Strikethrough~~text~~strikethroughStrike line; custom line color on iOS and web, Android always strikes in the text color (requires flavor="github")
Bold + italic***text*** (or ___text___)strong + emCombined emphasis
Links[text](url)linkOptional font family, color, background color, underline. Bare URLs autolink too, in both flavors. Per-URL overrides go through linkVariants
Inline code`code`codeMonospace font, background, optional fontSize
Inline images![alt](url)inlineImageInline images within the text flow (does not inherit block typography)
Inline math$...$inlineMathLaTeX math rendered within the text flow (requires md4cFlags={{ latexMath: true }}, on by default, plus the native math build)
Spoiler||text||spoilerText concealed behind an overlay; tap to reveal. Animated particles by default, or an opaque rectangle with spoilerOverlay="solid". It is always an inline span, never a block
Superscript^text^superscriptRaised text at a reduced font size (requires md4cFlags={{ superscript: true }})
Subscript~text~subscriptLowered text at a reduced font size (requires md4cFlags={{ subscript: true }})
Highlight==text==highlightHighlighted text with a background color (requires md4cFlags={{ highlight: true }})
note

Some delimiters are ambiguous, and how they parse depends on your md4cFlags:

  • Underscores (__text__, _text_) mean bold/italic by default. Enabling md4cFlags={{ underline: true }} takes _ away from emphasis entirely: _text_ becomes underline and __text__ becomes doubly underlined rather than bold. The * forms are unaffected, which is why they are the safer ones to author with - and why the "(or __text__)" / "(or ___text___)" alternatives in the table above do not hold with the flag on.
  • Single tildes (~text~) mean nothing by default. Enable md4cFlags={{ subscript: true }} to parse them as subscript. Double tildes (~~text~~) are strikethrough, but only under flavor="github" - under commonmark they render literally.
  • Double pipes (||text||) are always parsed as spoilers, so a || b || c renders b as a spoiler span rather than plain text.

Nesting​

Several block elements support unlimited nesting depth with automatic indentation:

  • Blockquotes - each level adds a new accent bar.
  • Unordered lists - each level indents with marginLeft.
  • Ordered lists - each level indents and maintains separate numbering.

Nested lists​

Indent a list item to nest it under the one above. Unordered and ordered lists can be nested to any depth, and each level is indented automatically:

- First level
- Second level
- Third level
- Fourth level (unlimited depth!)

1. First item
1. Nested numbered
1. Deep nested
2. Another nested
2. Second item

Lists with block content​

List items can contain block elements - fenced code blocks, nested lists, and multiple paragraphs:

1. Install via npm:

```
npm install
```

2. Verify the connection.

Code blocks indent to the item's content column, and the marker is drawn next to the first line even when a code block or nested list is the item's first child.

Nested blockquotes​

Add a > for each level of quoting. Every level draws its own accent bar, and a blockquote can hold any other Markdown - paragraphs, lists, or deeper quotes:

> Level 1 quote
> > Level 2 nested
> > > Level 3 nested (unlimited depth!)
note

The two flavors render blockquotes differently. Under flavor="github" a blockquote is a recursive container block: it draws its own box (padding on all sides, borderRadius, backgroundColor, and the accent border) and splits its content into segments, so a code block, table, or math block inside becomes a real nested block and each nesting level is its own box. Long-pressing one opens the copy menu. Under flavor="commonmark" blockquotes stay inside the single text view and are drawn with spans.

Superscript and subscript​

Superscript raises text and subscript lowers it, both at a reduced font size - useful for exponents, footnote markers, and chemical formulas:

E = mc^2^

H~2~O H~2~SO~4~

^14^C dating (isotope notation)

H~3~O^+^ (mixed superscript and subscript)
note

Superscript and subscript can be nested inside other inline elements such as bold, italic, and links, and inside each other in either direction (x^a~b~c^, x~a^b^c~). Nesting compounds both the font scale and the baseline shift, so two levels deep gets small fast.

Admonitions​

With flavor="github", a blockquote whose first line is an alert marker renders as a themed callout with an icon and a title header instead of a plain quote:

> [!NOTE]
> Useful information the reader should know.

> [!TIP]
> A helpful hint.

> [!IMPORTANT]
> Something essential to completing the task.

> [!WARNING]
> Urgent information needing immediate attention.

> [!CAUTION]
> Risks or negative outcomes of an action.

Those five markers are the whole set, and they are case-insensitive - > [!note] works the same as > [!NOTE]. A marker that is not one of the five is not an admonition: the block renders as an ordinary blockquote with the marker left in as literal text.

An admonition is still a blockquote, so it holds any block content - more paragraphs, lists, code blocks, even a video:

> [!WARNING]
> Check these before upgrading:
>
> - the peer dependency range
> - your `enriched-markdown` config block

Admonitions reuse the blockquote's geometry and override only its colors, so they are styled through markdownStyle.blockquote.admonitions - one color pair per type, on top of the blockquote box itself.

note

Requires both flavor="github" and the md4cFlags.admonitions flag. The flag is on by default, but it is forced off under flavor="commonmark", where > [!NOTE] renders as a plain blockquote with the marker as text.

Images: block vs. inline​

Images are automatically detected as block or inline based on context:

  • Block images use block-level spacing and sit on their own line.
  • Inline images align with the text baseline inside the surrounding run of text.

You don't need to specify which type - the renderer determines it from the image's position. Note that a single newline does not split a paragraph, so an image on its own source line directly below text is still inline; separate it with a blank line to make it a block image.

caution

The rule differs between native and web. On iOS and Android the decision is made from what precedes the image on its line: an image is a block image when nothing has been emitted before it on that line. On web it must be the only child of its paragraph. So ![alt](url) trailing text renders as a block image on native and an inline image on web.

important

CommonMark has no block image. ![alt](url) is defined as an inline element wherever it appears; the Images row in the Supported elements table above is listed under block elements only to describe what this renderer does with a standalone one.

The split is a rendering decision, not a parsing one: the parser emits the same Image node in both cases, and the renderer marks the ones that are inline. Anything reading the AST - your own traversal, or another CommonMark implementation - sees an inline image either way.

Videos​

Videos are embedded with an HTML <video> tag - the tag is the one piece of HTML the parser allowlists, and it is recognized only as a block-level element:

<video src="https://example.com/ocean.mp4" />

The paired form <video src="url"></video> works identically. Only src is read: width, height, controls, autoplay and every other attribute are ignored, so all appearance comes from markdownStyle.video. Playback controls come from the platform player - AVPlayerViewController on iOS, ExoPlayer on Android, and the browser's built-in <video> controls on web.

important

Quote the URL. An unquoted src ends at the first /, so <video src=https://example.com/a.mp4 /> parses as the URL https: and nothing loads.

note

On native, requires flavor="github" and the enableVideo build flag (on by default) - see Optional native features. Web has neither knob and always renders the player. No other HTML is rendered on any target: inline HTML is disabled and any other tag is ignored.

A video inside a blockquote or an admonition renders normally. A video inside a list item is different: it is promoted out of the list and rendered as a standalone block above the remaining items, without a bullet or number, which splits the list in two. The promotion happens in the parser, so web behaves the same way.

Line breaks​

Newlines follow standard CommonMark semantics by default:

  • Blank line - starts a new paragraph.
  • Single newline (soft break) - renders as a space; consecutive lines flow together into one wrapped paragraph, matching how GitHub and other CommonMark renderers display Markdown.
  • Hard break - end a line with two spaces or a backslash to force a line break within the paragraph.
  • <br> - not supported. Raw HTML is disabled, so an inline <br> stays in the output as literal text and one on its own line is dropped. Use a hard break, or hardSoftBreaks below, instead.
This line and
this line render as one continuous sentence.

A blank line starts a new paragraph.

Two trailing spaces
or a trailing backslash\
force a line break within the paragraph.

Preserving single newlines​

When displaying content authored in EnrichedMarkdownTextInput, pressing Enter produces a single newline in the serialized markdown. By default, EnrichedMarkdownText collapses these to spaces (per CommonMark). To preserve them as visible line breaks, enable the hardSoftBreaks flag:

<EnrichedMarkdownText
markdown={markdownFromInput}
md4cFlags={{ hardSoftBreaks: true }}
/>

This forces the parser to treat every soft break as a hard break, so single newlines render as line breaks on all platforms.

Blank lines​

By default, CommonMark collapses any run of consecutive blank lines between two blocks into a single paragraph break, so pressing Enter several times renders the same as pressing it once. To keep the extra spacing, enable the preserveBlankLines flag:

<EnrichedMarkdownText
markdown={markdownFromInput}
markdownStyle={{ paragraph: { marginTop: 0, marginBottom: 0 } }}
md4cFlags={{ preserveBlankLines: true }}
/>

Each blank line in the source renders as one empty line, so the output keeps the exact number of blank lines that were typed - four blank lines between two paragraphs render as four empty lines. This makes EnrichedMarkdownText reproduce content authored in EnrichedMarkdownTextInput line for line, which is useful for chat-style apps.

note

Paragraph margins stack on top of the blank-line spacing. Set paragraph.marginTop and paragraph.marginBottom to 0 in markdownStyle so that spacing is driven purely by blank lines; otherwise even a standard single-blank-line paragraph break will render with extra height.

tip

To reproduce editor content line for line, combine preserveBlankLines with hardSoftBreaks. See the Editor-style text guide for the full recipe.

What happens to an unsupported element​

Not every target renders every element, and the three renderers fail in three different ways. This matters most for spoilers on web and for math in a build compiled without the native math engine.

TargetUnmapped node
iOS / macOSRecurses into the node's children, so the text survives without its styling
AndroidFalls back to a plain text renderer that emits only the node's own content and never visits children, so an unsupported container loses all of its text
WebDrops the node and its whole subtree
danger

The clearest case is spoilers on web: the parser emits the node, the web renderer has no entry for it, so the concealed text is deleted rather than concealed - a ||secret|| b renders as a b. Do not put content behind a spoiler if the same Markdown is rendered on web.

Reading the AST​

If you traverse the parsed tree yourself, these are the node type names the parser emits:

Document, Paragraph, Text, Link, Heading, LineBreak, Strong, Emphasis, Strikethrough, Underline, Code, Image, Blockquote, UnorderedList, OrderedList, ListItem, CodeBlock, ThematicBreak, Table, TableHead, TableBody, TableRow, TableHeaderCell, TableCell, LatexMathInline, LatexMathDisplay, Spoiler, Superscript, Subscript, Highlight, SoftBreak, BlankLine, Admonition, Video.

Two things are easy to miss: there is no TaskList node (a task item is a ListItem with isTask and taskChecked), and tables expand into five structural node types of their own. Match on the names rather than on any numeric ordinal - the ordinals are insertion order and new types are appended.

Try it yourself​

Loading...
import { EnrichedMarkdownText } from 'react-native-enriched-markdown';
import { View, StyleSheet, Linking, useColorScheme } from 'react-native';
import { useMemo } from 'react';
import { defaultMarkdownStyle } from './theme';

const markdown = `# Welcome to Markdown!

I can render a paragraph with **bold**, *italic*, and a [link](https://enriched.swmansion.com/markdown).

> Blockquotes work too.

- List item one
- List item two
- Nested item
`;

export default function App() {
const isDark = useColorScheme() === 'dark';

// Start from the shared default palette (it follows the color scheme) and
// override just the elements you want. Every element is styleable this way -
// tweak a value and watch the preview update.
const markdownStyle = useMemo(
() => ({
...defaultMarkdownStyle(isDark),
link: { color: isDark ? '#57b495' : '#3f9e82' },
}),
[isDark]
);

return (
<View style={styles.container}>
<EnrichedMarkdownText
markdown={markdown}
markdownStyle={markdownStyle}
onLinkPress={({ url }) => Linking.openURL(url)}
/>
</View>
);
}

const styles = StyleSheet.create({
container: { gap: 12 },
});