markdown-to-jsx vs react-markdown vs remark-react
Rendering Markdown Content in React Applications
markdown-to-jsxreact-markdownremark-react

Rendering Markdown Content in React Applications

markdown-to-jsx, react-markdown, and remark-react are libraries designed to render Markdown content as React components. markdown-to-jsx is a lightweight, zero-dependency parser that converts Markdown directly into React elements, prioritizing small bundle size and simplicity. react-markdown is the most popular solution, built on the remark ecosystem, offering a robust plugin architecture for syntax highlighting, custom components, and security features like sanitization. remark-react was historically the low-level React renderer for the remark processor but has been deprecated; its functionality has been fully merged into react-markdown, making it obsolete for new projects.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
markdown-to-jsx02,3875 MB15a month agoMIT
react-markdown015,87752.6 kB52 years agoMIT
remark-react0524567 B02 years ago-

Rendering Markdown in React: markdown-to-jsx vs react-markdown vs remark-react

When adding Markdown support to a React application, the choice of library dictates your bundle size, security posture, and ability to customize the output. While markdown-to-jsx and react-markdown are both active and viable, they take fundamentally different approaches to parsing and rendering. remark-react, once a key piece of the puzzle, is now obsolete. Let's break down the technical realities of each.

πŸ—οΈ Architecture: Standalone Parser vs Plugin Ecosystem

The core difference lies in how these libraries process text. markdown-to-jsx is a self-contained engine. It includes its own parser and renderer, meaning you install one package and get everything you need. This keeps things simple but limits extensibility.

react-markdown is different. It is a React renderer for the unified ecosystem. It doesn't parse Markdown itself; it relies on plugins like remark-parse to create an Abstract Syntax Tree (AST), which it then walks to generate React elements. This architecture allows you to swap out parsers or inject plugins to modify the AST before rendering.

// markdown-to-jsx: Self-contained, no plugins needed for basic usage
import Markdown from 'markdown-to-jsx';

<Markdown>{'# Hello World'}</Markdown>
// react-markdown: Requires explicit plugins for extended features
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm'; // Plugin for GitHub Flavored Markdown

<ReactMarkdown remarkPlugins={[remarkGfm]}>{'# Hello World'}</ReactMarkdown>
// remark-react: DEPRECATED - Do not use in new code
// This package is archived. Functionality moved to react-markdown.
// import remarkReact from 'remark-react'; // ❌ Avoid

🎨 Customizing Components: Overriding HTML Tags

In real-world apps, you rarely want default HTML tags. You might want your <h1> to be a specific styled component or your links to open in new tabs automatically. Both active libraries support this, but the API differs slightly.

markdown-to-jsx uses an options prop with an overrides object. You map the HTML tag name to a React component or configuration object.

// markdown-to-jsx: Override via options
import Markdown from 'markdown-to-jsx';

const options = {
  overrides: {
    h1: { component: 'h1', props: { className: 'text-3xl font-bold' } },
    a: { component: (props) => <a target="_blank" rel="noopener" {...props} /> }
  }
};

<Markdown options={options}>{'[Link](https://example.com)'}</Markdown>

react-markdown uses a components prop. You pass an object where keys are HTML tag names and values are React components. This feels more idiomatic to modern React patterns.

// react-markdown: Override via components prop
import ReactMarkdown from 'react-markdown';

const components = {
  h1: ({node, ...props}) => <h1 className="text-3xl font-bold" {...props} />,
  a: ({node, ...props}) => <a target="_blank" rel="noopener" {...props} />
};

<ReactMarkdown components={components}>{'[Link](https://example.com)'}</ReactMarkdown>
// remark-react: DEPRECATED
// Previously used .use() chain with custom compilers. 
// No longer maintained or recommended.

πŸ”’ Security: Sanitization and XSS Protection

Rendering user-generated Markdown introduces Cross-Site Scripting (XSS) risks if dangerous HTML tags (like <script>) are allowed. How each library handles this is critical.

markdown-to-jsx does not sanitize HTML by default. It renders whatever HTML the Markdown produces. If you accept user input, you must wrap it with a sanitizer like dompurify or use its built-in disableParsingRawHTML option carefully, though relying on external sanitization is safer for dynamic content.

// markdown-to-jsx: Manual sanitization required for user content
import DOMPurify from 'dompurify';
import Markdown from 'markdown-to-jsx';

const safeContent = DOMPurify.sanitize(userInput);
<Markdown>{safeContent}</Markdown>

react-markdown strips dangerous HTML nodes by default. It only allows a safe whitelist of tags. If you need to allow specific dangerous tags (like <iframe>), you must explicitly configure them, which forces you to make a conscious security decision.

// react-markdown: Safe by default, strict whitelist
import ReactMarkdown from 'react-markdown';

// Automatically strips <script>, <iframe>, etc.
<ReactMarkdown>{userInput}</ReactMarkdown>

// To allow specific extra tags (use with caution)
<ReactMarkdown allowedElements={['iframe', 'script']}>{userInput}</ReactMarkdown>
// remark-react: DEPRECATED
// Had similar sanitization concepts but is no longer updated 
// to handle modern XSS vectors or React security best practices.

🧩 Extensibility: Syntax Highlighting and GFM

Support for GitHub Flavored Markdown (tables, strikethrough, task lists) and syntax highlighting is a common requirement.

markdown-to-jsx supports GFM features out of the box in recent versions without extra plugins. However, syntax highlighting usually requires writing a custom renderer for code blocks or using a specific fork/plugin, which can be cumbersome.

// markdown-to-jsx: GFM works by default
import Markdown from 'markdown-to-jsx';

// Tables and task lists work immediately
<Markdown>{'| Col A | Col B |\n|---|---|\n| 1 | 2 |'}</Markdown>

react-markdown requires plugins for GFM and syntax highlighting. This adds to the bundle size but gives you precise control. You combine remark-gfm for tables and rehype-highlight (or similar) for code coloring.

// react-markdown: Plugins required for GFM and highlighting
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeHighlight from 'rehype-highlight';

<ReactMarkdown 
  remarkPlugins={[remarkGfm]} 
  rehypePlugins={[rehypeHighlight]}
>
  {content}
</ReactMarkdown>
// remark-react: DEPRECATED
// Was the original way to attach these plugins to React.
// Now redundant as react-markdown absorbs this role entirely.

βš–οΈ Similarities: Shared Ground

Despite their architectural differences, markdown-to-jsx and react-markdown share common goals and behaviors.

1. βš›οΈ React Component Output

Both libraries output standard React elements. You can nest them, style them with CSS modules or Tailwind, and treat them like any other component in your tree.

// Both render standard JSX
<div className="prose">
  <Markdown>{text}</Markdown> {/* or <ReactMarkdown>{text}</ReactMarkdown> */}
</div>

2. πŸ“ Basic Markdown Support

Both handle core Markdown syntax (headers, lists, bold, italic, links, images) perfectly. For simple documentation or static content, the visual output is nearly identical.

// Both handle basic syntax identically
const basic = '# Title\n
* List item';
// Renders same visual result in both

3. πŸ”Œ Async Content Handling

Neither library handles data fetching. You fetch your Markdown string (from an API or file) in your component logic and pass it as a prop. They are purely presentational.

// Pattern for both: Fetch then render
const [content, setContent] = useState('');

useEffect(() => {
  fetch('/api/post.md').then(r => r.text()).then(setContent);
}, []);

return <Markdown>{content}</Markdown>; // Or ReactMarkdown

πŸ“Š Summary: Key Differences

Featuremarkdown-to-jsxreact-markdownremark-react
Statusβœ… Activeβœ… Active❌ Deprecated
DependenciesZero (Standalone)High (Unified/Remark ecosystem)High (Legacy)
Bundle SizeSmall (~5kb gzipped)Larger (depends on plugins)N/A
GFM SupportBuilt-inVia remark-gfm pluginLegacy plugin
SanitizationManual (Default: Unsafe)Automatic (Default: Safe)Legacy
Customizationoptions.overridescomponents prop + AST pluginsDeprecated API

πŸ’‘ The Big Picture

markdown-to-jsx is the pragmatic choice for lightweight needs. If you are building a small site, a changelog, or a component where every kilobyte counts, and you don't need complex AST manipulations, this is your tool. It gets the job done with minimal setup.

react-markdown is the enterprise-grade solution. If you are building a platform with user-generated content, need strict security defaults, require syntax highlighting, or want to tap into the massive unified plugin ecosystem, this is the only serious option. The initial setup is heavier, but the flexibility pays off in complex scenarios.

remark-react is history. It served a purpose when the unified ecosystem was splitting responsibilities more strictly. Today, it is a deprecated package that should be removed from any package.json. Its features live on in react-markdown.

Final Thought: For most professional React applications today, react-markdown is the standard due to its security defaults and extensibility. Only reach for markdown-to-jsx if you have proven bundle size constraints and simple requirements. Never start a new project with remark-react.

How to Choose: markdown-to-jsx vs react-markdown vs remark-react

  • markdown-to-jsx:

    Choose markdown-to-jsx if you need a lightweight, zero-dependency solution for rendering basic to intermediate Markdown without the overhead of a complex plugin ecosystem. It is ideal for simple blogs, documentation sites, or components where bundle size is a critical constraint and you do not need advanced syntax extensions like GFM tables or math formulas via plugins.

  • react-markdown:

    Choose react-markdown if you require a mature, extensible solution with a rich ecosystem of plugins for syntax highlighting, custom component mapping, and security sanitization. It is the best fit for complex applications where you need to support GitHub Flavored Markdown (GFM), modify the Abstract Syntax Tree (AST), or integrate deeply with the unified/remark toolchain.

  • remark-react:

    Do NOT choose remark-react for any new project. This package has been officially deprecated and archived, with its core functionality merged directly into react-markdown. Using it introduces unnecessary technical debt and potential security risks due to lack of maintenance. Migrate any existing usage to react-markdown immediately.

README for markdown-to-jsx

npm version downloads

markdown-to-jsx is a gfm+commonmark compliant markdown parser and compiler toolchain for JavaScript and TypeScript-based projects. It is extremely fast, capable of processing large documents fast enough for real-time interactivity.

Some special features of the library:

  • Arbitrary HTML is supported and parsed into the appropriate JSX representation without dangerouslySetInnerHTML

  • Any HTML tags rendered by the compiler and/or <Markdown> component can be overridden to include additional props or even a different HTML representation entirely.

  • All GFM special syntaxes are supported, including tables, task lists, strikethrough, autolinks, tag filtering, and more.

  • Fenced code blocks with highlight.js support; see Syntax highlighting for instructions on setting up highlight.js.

Table of Contents

Upgrading

From v8.x to v9.x

Breaking Changes:

  • ast option removed: The ast: true option on compiler() has been removed. Use the new parser() function instead to access the AST directly.
/** v8 */ compiler('# Hello world', { ast: true })
/** v9 */ parser('# Hello world')
  • namedCodesToUnicode option removed: The namedCodesToUnicode option has been removed. All named HTML entities are now supported by default via the full entity list, so custom entity mappings are no longer needed.
/** v8 */ compiler('&le; symbol', { namedCodesToUnicode: { le: '\u2264' } })
/** v9 */ compiler('&le; symbol')
  • tagfilter enabled by default: Dangerous HTML tags (script, iframe, style, title, textarea, xmp, noembed, noframes, plaintext) are escaped by default across the React, HTML, Solid, Vue, and React Native outputs. Matching GFM, only each tag's leading < is neutralized; the body and closing tag stay visible as inert text, and allowed nested tags inside a filtered parent still render normally. Previously some renderers mounted these tags live, and escaped output dropped everything after the opener.
/** v8 */ tags rendered as JSX elements
/** v9 */ tags escaped by default (full source kept as inert text)
compiler('<script>alert("xss")</script>')
// React: <span>&lt;script&gt;alert("xss")&lt;/script&gt;</span>
// HTML:  &lt;script>alert("xss")&lt;/script>

/** Restore old behavior */
compiler('<script>alert("xss")</script>', { tagfilter: false })

New Features:

  • New parser function: Provides direct access to the parsed AST without rendering. This is the recommended way to get AST nodes.

  • New entry points: React-specific, HTML-specific, and markdown-specific entry points are now available for better tree-shaking and separation of concerns.

// React-specific usage
import Markdown, { compiler, parser } from 'markdown-to-jsx/react'

// HTML string output
import { compiler, astToHTML, parser } from 'markdown-to-jsx/html'

// Markdown string output (round-trip compilation)
import { compiler, astToMarkdown, parser } from 'markdown-to-jsx/markdown'

Migration Guide:

  1. Replace compiler(..., { ast: true }) with parser():
/** v8 */ compiler(markdown, { ast: true })
/** v9 */ parser(markdown)
  1. Migrate React imports to /react entry point (optional but recommended):
/** Legacy */ import from 'markdown-to-jsx'
/** Recommended */ import from 'markdown-to-jsx/react'
  1. Remove namedCodesToUnicode option: All named HTML entities are now supported automatically, so you can remove any custom entity mappings.
/** v8 */ compiler('&le; symbol', { namedCodesToUnicode: { le: '\u2264' } })
/** v9 */ compiler('&le; symbol')

Note: The main entry point (markdown-to-jsx) continues to work for backward compatibility, but React code there is deprecated and will be removed in a future major release. Consider migrating to markdown-to-jsx/react for React-specific usage.

### Older Migration Guides

From v7.x to v8.x

Breaking Changes:

  • Type ParserResult renamed to ASTNode - If you were using MarkdownToJSX.ParserResult in your code, update to MarkdownToJSX.ASTNode
/** v7 */ MarkdownToJSX.ParserResult[]
/** v8+ */ MarkdownToJSX.ASTNode[]
  • Multiple RuleType enums consolidated into RuleType.textFormatted - If you were checking for RuleType.textBolded, RuleType.textEmphasized, RuleType.textMarked, or RuleType.textStrikethroughed, update to check for RuleType.textFormatted and inspect the node's boolean flags:
/** v7 */ RuleType.textBolded
/** v8+ */ RuleType.textFormatted && node.bold

Installation

Install markdown-to-jsx with your favorite package manager.

npm i markdown-to-jsx

Usage

markdown-to-jsx exports a React component by default for easy JSX composition:

ES6-style usage*:

import Markdown from 'markdown-to-jsx'
import React from 'react'
import { render } from 'react-dom'

render(<Markdown># Hello world!</Markdown>, document.body)

/*
    renders:

    <h1>Hello world!</h1>
 */

* NOTE: JSX does not natively preserve newlines in multiline text. In general, writing markdown directly in JSX is discouraged and it's a better idea to keep your content in separate .md files and require them, perhaps using webpack's raw-loader.

Entry Points

markdown-to-jsx provides multiple entry points for different use cases:

Main

The legacy default entry point exports everything, including the React compiler and component:

import Markdown, { compiler, parser } from 'markdown-to-jsx'

The React code in this entry point is deprecated and will be removed in a future major release, migrate to markdown-to-jsx/react.

React

For React-specific usage, import from the /react entry point:

import Markdown, { compiler, parser, astToJSX } from 'markdown-to-jsx/react'

const jsxElement = compiler('# Hello world')

function App() {
  return <Markdown children="# Hello world" />
}

/** Or use parser + astToJSX */
const ast = parser('# Hello world')
const jsxElement2 = astToJSX(ast)
React Server Components (RSC)

The Markdown component automatically detects whether it's running in a React Server Component (RSC) or client environment and adapts accordingly. No 'use client' directive is required.

Server Component (RSC) usage:

// Server Component - works automatically
import Markdown from 'markdown-to-jsx/react'

export default async function Page() {
  const content = await fetchMarkdownContent()
  return <Markdown>{content}</Markdown>
}

Client Component usage:

// Client Component - also works automatically
'use client'
import Markdown from 'markdown-to-jsx/react'

export function ClientMarkdown({ content }: { content: string }) {
  return <Markdown>{content}</Markdown>
}

Notes:

  • MarkdownProvider and MarkdownContext are client-only and become no-ops in RSC environments
  • RSC rendering provides better performance by avoiding client-side hydration
  • The component maintains identical output in both environments
  • No migration needed for existing code

React Native

For React Native usage, import from the /native entry point:

import Markdown, { compiler, parser, astToNative } from 'markdown-to-jsx/native'
import { View, Text, StyleSheet, Linking } from 'react-native'

const nativeElement = compiler('# Hello world', {
  styles: {
    heading1: { fontSize: 32, fontWeight: 'bold' },
    paragraph: { marginVertical: 8 },
    link: { color: 'blue', textDecorationLine: 'underline' },
  },
  onLinkPress: url => {
    Linking.openURL(url)
  },
})

const markdown = `# Hello world

This is a [link](https://example.com) with **bold** and *italic* text.
`

function App() {
  return (
    <View>
      <Markdown
        children={markdown}
        options={{
          styles: StyleSheet.create({
            heading1: { fontSize: 32, fontWeight: 'bold' },
            paragraph: { marginVertical: 8 },
            link: { color: 'blue', textDecorationLine: 'underline' },
          }),
          onLinkPress: url => {
            Linking.openURL(url)
          },
        }}
      />
    </View>
  )
}

React Native-specific options:

  • onLinkPress?: (url: string, title?: string) => void - Custom handler for link presses (defaults to Linking.openURL)
  • onLinkLongPress?: (url: string, title?: string) => void - Handler for link long presses
  • styles?: NativeStyles - Per-key style overrides keyed by element type. Each key is narrowed to the style accepted by its target component (TextStyle for inline content and headings, ViewStyle for containers, ImageStyle for images).
  • wrapperProps?: ViewProps | TextProps - Props for the wrapper component (defaults to View for block, Text for inline)

Default styles:

React Native output ships with a clean, minimal base stylesheet so markdown renders with a readable hierarchy out of the box: a heading size cascade, monospace code, spacing between blocks, a blockquote rule, and a table with a header row and aligned columns. Everything stays customizable. Each key in styles merges over the default for that element, so setting one property (say heading1.color) keeps the rest of the default. styles.text is a base applied under all rendered text, so you can set the font, color, and size for the whole document at once:

<Markdown options={{ styles: { text: { color: '#333', fontSize: 17 } } }}>{content}</Markdown>

Override any element by its key. Every key is optional and merges over the default, so you change only the properties you name:

<Markdown
  options={{
    styles: {
      heading1: { color: '#b91c1c' }, // change one property, keep the rest of the default
      blockquote: { borderLeftColor: '#b91c1c' },
      codeInline: { backgroundColor: '#f4f4f5' },
    },
  }}
>
  {content}
</Markdown>

The available keys, each typed to the style its target component accepts (TextStyle, ViewStyle, or ImageStyle):

  • Text: text (the base under all rendered text), paragraph, heading1 through heading6, link, footnote, codeInline, strong, em, del, mark, listItemBullet, listItemNumber
  • Blocks: blockquote, codeBlock, thematicBreak, image
  • Lists: listOrdered, listUnordered, listItem
  • Tables: table, tableHeader, tableHeaderCell, tableHeaderText (the bold header run), tableRow, tableCell, tableCellDivider and tableRowDivider (the grid lines)
  • GFM task: gfmTask (the drawn checkbox), gfmTaskChecked (the checked-state accent fill), checkmark (the checkmark glyph)

Raw HTML container tags (div, section, article, ul, ol, li, th, td, and similar) also take a style under their own tag name.

To replace an element's rendering entirely rather than restyle it, use overrides. Here is a fully themed configuration combining styles and overrides, plus a renderRule that swaps fenced code for a syntax highlighter:

const Callout = ({ children }) => (
  <View style={{ borderLeftColor: '#3fb950', borderLeftWidth: 4, paddingLeft: 12 }}>{children}</View>
)

<Markdown
  options={{
    styles: {
      text: { color: '#c9d1d9' }, // base color for all text
      heading1: { color: '#f0f6fc', fontSize: 26 },
      link: { color: '#58a6ff', textDecorationLine: 'none' },
    },
    overrides: { blockquote: { component: Callout } },
    wrapperProps: { style: { backgroundColor: '#0d1117', padding: 12 } },
    renderRule(next, node, _renderChildren, state) {
      if (node.type === RuleType.codeBlock) {
        return <MySyntaxHighlighter key={state.key} code={node.text} lang={node.lang} />
      }
      return next()
    },
  }}
>
  {content}
</Markdown>

A fenced code block renders as <pre><code>, and neither element receives the fence's language, so language-aware code rendering (syntax highlighting, a KaTeX block for ```latex) belongs in renderRule, where node.lang and node.text are available.

Overrides:

Overrides on native work the same as on web: overrides keys correspond to HTML tag names and fire for parsed markdown as well as raw HTML. For example, override code to swap inline backticks and the inner element of fenced code blocks, override pre to wrap fenced code, override input to render real checkbox visuals for GFM tasks, and override ul/ol/li to swap list containers and rows. Bullets and numbers remain library-controlled inside li.

When both a renderer-supplied style (styles.codeInline, styles.gfmTask, etc.) and overrides[tag].props.style are set, they merge as a React Native style array, and override-level styling wins on conflict.

GFM task checkboxes:

Task checkboxes (- [x], - [ ]) route through an <input type="checkbox"> tag that maps to View by default and renders a drawn checkbox: an outlined box that fills with an accent color and a checkmark when the task is done. The checkbox stands in for the list bullet, which is suppressed for task items. Restyle the box with styles.gfmTask, or override input to replace the visual entirely (for a real <Image> checkbox, animated state, etc.). Your override receives checked, type: 'checkbox', readOnly, the merged style, and a <Text> child rendering [x] or [ ] as a fallback marker; consumers that fully customize the visual should ignore the child and render their own indicator from props.checked.

The list item wrapper around a task gets flexDirection: 'row' and alignItems: 'flex-start' applied by default, so the checkbox lines up with the first line of the label; the checkbox's own top margin then centers it against that line, which keeps it correctly placed even when the label wraps to multiple lines. Override these by passing your own styles.listItem: mergeStyle keeps the row defaults underneath, so any property you set wins on collision (e.g. supply alignItems: 'center' to vertically center the checkbox against the whole label instead).

HTML Tag Mapping: HTML tags are automatically mapped to React Native components:

  • <img> β†’ Image component
  • Block elements (<div>, <section>, <article>, <blockquote>, <hr>, <input>, <ul>, <ol>, <li>, <table>, etc.) β†’ View component
  • Inline elements (<span>, <strong>, <em>, <a>, headings, <code>, etc.) β†’ Text component
  • Type 1 blocks (<pre>, <script>, <style>, <textarea>) β†’ View component

Mixing text and blocks: React Native cannot nest an image or view inside text, so an inline container (a paragraph, heading, emphasis, or link) that holds an image or block-level content renders as a View instead of a Text. Its text is grouped into a Text so it still flows on one line, the image renders as its own element, and an image inside a link stays tappable through a Pressable. Text-only content is unaffected and renders as before.

Note: Links are underlined by default for better accessibility and discoverability. You can override this via the styles.link option.

Note: Footnote reference markers ([^1]) render as a superscript, matching the <sup> the web renderers emit. React Native cannot raise or shrink inline text through styles (it ignores verticalAlign and transforms on inline text), so numeric markers use Unicode superscript glyphs (ΒΉΒ²Β³), which the font draws raised and sized relative to the surrounding text on their own. Non-numeric identifiers ([^note]) render as plain text. Style the marker via the styles.footnote option.

SolidJS

For SolidJS usage, import from the /solid entry point:

import Markdown, {
  compiler,
  parser,
  astToJSX,
  MarkdownProvider,
} from 'markdown-to-jsx/solid'
import { createSignal } from 'solid-js'

// Static content
const solidElement = compiler('# Hello world')

function App() {
  return <Markdown children="# Hello world" />
}

// Reactive content (automatically updates when content changes)
function ReactiveApp() {
  const [content, setContent] = createSignal('# Hello world')
  return <Markdown>{content}</Markdown>
}

// Or use parser + astToJSX
const ast = parser('# Hello world')
const solidElement2 = astToJSX(ast)

// Use context for default options
function AppWithContext() {
  return (
    <MarkdownProvider options={{ sanitizer: customSanitizer }}>
      <Markdown># Content</Markdown>
    </MarkdownProvider>
  )
}

SolidJS-specific features:

  • Reactive content: The Markdown component accepts signals/accessors for automatic updates when markdown content changes
  • Memoization: AST parsing is automatically memoized for optimal performance
  • Context API: Use MarkdownProvider to provide default options and avoid prop drilling

Vue.js

For Vue.js 3 usage, import from the /vue entry point:

import Markdown, { compiler, parser, astToJSX } from 'markdown-to-jsx/vue'
import { h } from 'vue'

// Using compiler
const vnode = compiler('# Hello world')

// Using component
<Markdown children="# Hello world" />

// Or use parser + astToJSX
const ast = parser('# Hello world')
const vnode2 = astToJSX(ast)

Vue.js-specific features:

  • Vue 3 support: Uses Vue 3's h() render function API
  • JSX support: Works with Vue 3 JSX via @vue/babel-plugin-jsx or @vitejs/plugin-vue-jsx
  • HTML attributes: Uses standard HTML attributes (class instead of className)
  • Component overrides: Support for both Options API and Composition API components

HTML

For HTML string output (server-side rendering), import from the /html entry point:

import { compiler, astToHTML, parser } from 'markdown-to-jsx/html'

const htmlString = compiler('# Hello world')

/** Or use parser + astToHTML */
const ast = parser('# Hello world')
const htmlString2 = astToHTML(ast)

Markdown

For markdown-to-markdown compilation (normalization and formatting), import from the /markdown entry point:

import { compiler, astToMarkdown, parser } from 'markdown-to-jsx/markdown'

const normalizedMarkdown = compiler('# Hello  world\n\nExtra spaces!')

/** Or work with AST */
const ast = parser('# Hello  world')
const normalizedMarkdown2 = astToMarkdown(ast)

Library Options

All Options

OptionTypeDefaultDescription
createElementfunction-Custom createElement behavior (React/React Native/SolidJS/Vue only). See createElement for details.
disableAutoLinkbooleanfalseDisable automatic conversion of bare URLs to anchor tags.
disableParsingRawHTMLbooleanfalseDisable parsing of raw HTML into JSX.
enforceAtxHeadingsbooleanfalseRequire space between # and header text (GFM spec compliance).
evalUnserializableExpressionsbooleanfalse⚠️ Eval unserializable props (DANGEROUS). See evalUnserializableExpressions for details.
forceBlockbooleanfalseForce all content to be treated as block-level.
forceInlinebooleanfalseForce all content to be treated as inline.
ignoreHTMLBlocksbooleanfalseDisable parsing of HTML blocks, treating them as plain text.
forceWrapperbooleanfalseForce wrapper even with single child (React/React Native/Vue only). See forceWrapper for details.
overridesobject-Override HTML tag rendering. See overrides for details.
preserveFrontmatterbooleanfalseInclude frontmatter in rendered output (as <pre> for HTML/JSX, included in markdown). Behavior varies by compiler type.
renderRulefunction-Custom rendering for AST rules. See renderRule for details.
sanitizerfunctionbuilt-inCustom URL sanitizer function. See sanitizer for details.
slugifyfunctionbuilt-inCustom slug generation for heading IDs. See slugify for details.
optimizeForStreamingbooleanfalseSuppress rendering of incomplete markdown syntax for streaming. See Streaming Markdown for details.
tagfilterbooleantrueEscape dangerous HTML tags (script, iframe, style, etc.) to prevent XSS.
wrapperstring | component | null'div'Wrapper element for multiple children (React/React Native/Vue only). See wrapper for details.
wrapperPropsobject-Props for wrapper element (React/React Native/Vue only). See wrapperProps for details.

options.createElement

Sometimes, you might want to override the React.createElement default behavior to hook into the rendering process before the JSX gets rendered. This might be useful to add extra children or modify some props based on runtime conditions. The function mirrors the React.createElement function, so the params are type, [props], [...children]:

import Markdown from 'markdown-to-jsx'
import React from 'react'
import { render } from 'react-dom'

const md = `
# Hello world
`

render(
  <Markdown
    children={md}
    options={{
      createElement(type, props, children) {
        return (
          <div className="parent">
            {React.createElement(type, props, children)}
          </div>
        )
      },
    }}
  />,
  document.body
)

options.forceWrapper

By default, the compiler does not wrap the rendered contents if there is only a single child. You can change this by setting forceWrapper to true. If the child is inline, it will not necessarily be wrapped in a span.

// Using `forceWrapper` with a single, inline child…
<Markdown options={{ wrapper: 'aside', forceWrapper: true }}>
  Mumble, mumble…
</Markdown>

// renders

<aside>Mumble, mumble…</aside>

options.overrides

Override HTML tag rendering or render custom React components. Three use cases:

1. Remove tags: Return null to completely remove tags (beyond tagfilter escaping):

<Markdown options={{ overrides: { iframe: () => null } }}>
  <iframe src="..."></iframe>
</Markdown>

2. Override HTML tags: Change component, props, or both:

const MyParagraph = ({ children, ...props }) => <div {...props}>{children}</div>

<Markdown options={{ overrides: { h1: { component: MyParagraph, props: { className: 'foo' } } } }}>
  # Hello
</Markdown>

/** Simplified */ { overrides: { h1: MyParagraph } }

3. Render React components: Use custom components in markdown:

import DatePicker from './date-picker'

const md = `<DatePicker timezone="UTC+5" startTime={1514579720511} />`

<Markdown options={{ overrides: { DatePicker } }}>{md}</Markdown>

Important notes:

  • JSX props are intelligently parsed (v9.1+):
    • Arrays and objects: data={[1, 2, 3]} β†’ parsed as [1, 2, 3]
    • Booleans: enabled={true} β†’ parsed as true
    • Functions: onClick={() => ...} β†’ kept as string for security (use renderRule for case-by-case handling, or see evalUnserializableExpressions)
    • Complex expressions: value={someVar} β†’ kept as string
  • Some props are preserved: a (href, title), img (src, alt, title), input[type="checkbox"] (checked, readonly), ol (start), td/th (style)
  • Element mappings: span for inline text, code for inline code, pre > code for code blocks

options.evalUnserializableExpressions

⚠️ SECURITY WARNING: STRONGLY DISCOURAGED FOR USER INPUTS

When enabled, attempts to eval expressions in JSX props that cannot be serialized as JSON (functions, variables, complex expressions). This uses eval() which can execute arbitrary code.

By default (recommended), unserializable expressions are kept as strings for security:

import { parser } from 'markdown-to-jsx'

const ast = parser('<Button onClick={() => alert("hi")} />')
// ast[0].attrs.onClick === "() => alert(\"hi\")" (string, safe)

// Arrays and objects are automatically parsed (no eval needed):
const ast2 = parser('<Table data={[1, 2, 3]} />')
// ast2[0].attrs.data === [1, 2, 3] (parsed via JSON.parse)

ONLY enable this option when:

  • The markdown source is completely trusted (e.g., your own documentation)
  • You control all JSX components and their props
  • The content is NOT user-generated or user-editable

DO NOT enable this option when:

  • Processing user-submitted markdown
  • Rendering untrusted content
  • Building public-facing applications with user content

Example of the danger:

// User-submitted markdown with malicious code
const userMarkdown = '<Component onClick={() => fetch("/admin/delete-all")} />'

// ❌ DANGEROUS - function will be executable
parser(userMarkdown, { evalUnserializableExpressions: true })

// βœ… SAFE - function kept as string
parser(userMarkdown) // default behavior

Safe alternative: Use renderRule for case-by-case handling:

// Instead of eval'ing arbitrary expressions, handle them selectively in renderRule:
const handlers = {
  handleClick: () => console.log('clicked'),
  handleSubmit: () => console.log('submitted'),
}

compiler(markdown, {
  renderRule(next, node) {
    if (
      node.type === RuleType.htmlBlock &&
      typeof node.attrs?.onClick === 'string'
    ) {
      // Option 1: Named handler lookup (safest)
      const handler = handlers[node.attrs.onClick]
      if (handler) {
        return <button onClick={handler}>{/* ... */}</button>
      }

      // Option 2: Selective eval with allowlist (still risky)
      if (
        node.tag === 'TrustedComponent' &&
        node.attrs.onClick.startsWith('() =>')
      ) {
        try {
          const fn = eval(`(${node.attrs.onClick})`)
          return <button onClick={fn}>{/* ... */}</button>
        } catch (e) {
          // Handle error
        }
      }
    }
    return next()
  },
})

This approach gives you full control over which expressions are evaluated and under what conditions.

options.ignoreHTMLBlocks

When enabled, the parser will not attempt to parse HTML blocks. HTML syntax will be treated as plain text and rendered as-is.

<Markdown options={{ ignoreHTMLBlocks: true }}>
  {'<div class="custom">This will be rendered as text</div>'}
</Markdown>

options.renderRule

Supply your own rendering function that can selectively override how rules are rendered (note, this is different than options.overrides which operates at the HTML tag level and is more general). The renderRule function always executes before any other rendering code, giving you full control over how nodes are rendered, including normally-skipped nodes like ref, footnote, and frontmatter.

You can use this functionality to do pretty much anything with an established AST node; here's an example of selectively overriding the "codeBlock" rule to process LaTeX syntax using the @matejmazur/react-katex library:

import Markdown, { RuleType } from 'markdown-to-jsx'
import TeX from '@matejmazur/react-katex'

const exampleContent =
  'Some important formula:\n\n```latex\nmathbb{N} = { a in mathbb{Z} : a > 0 }\n```\n'

function App() {
  return (
    <Markdown
      children={exampleContent}
      options={{
        renderRule(next, node, renderChildren, state) {
          if (node.type === RuleType.codeBlock && node.lang === 'latex') {
            return (
              <TeX as="div" key={state.key}>{String.raw`${node.text}`}</TeX>
            )
          }

          return next()
        },
      }}
    />
  )
}

Accessing parsed HTML content: For HTML blocks (like <script>, <style>, <pre>), renderRule can access the fully parsed AST in children:

<Markdown
  options={{
    renderRule(next, node, renderChildren) {
      if (node.type === RuleType.htmlBlock && node.tag === 'script') {
        // Access parsed children for custom rendering
        const parsedContent = node.children || []
        return <CustomScript content={parsedContent} />
      }
      return next()
    },
  }}
>
  <script>Hello **world**</script>
</Markdown>

options.sanitizer

By default a lightweight URL sanitizer function is provided to avoid common attack vectors that might be placed into the href of an anchor tag, for example. The sanitizer receives the input, the HTML tag being targeted, and the attribute name. The original function is available as a library export called sanitizer.

This can be overridden and replaced with a custom sanitizer if desired via options.sanitizer:

// sanitizer in this situation would receive:
// ('javascript:alert("foo")', 'a', 'href')

<Markdown options={{ sanitizer: (value, tag, attribute) => value }}>
  {`[foo](javascript:alert("foo"))`}
</Markdown>

// or

compiler('[foo](javascript:alert("foo"))', {
  sanitizer: value => value,
})

Raw HTML sanitization

Raw HTML in your markdown is sanitized automatically in every renderer (React, React Native, HTML, Markdown, Solid, and Vue), so untrusted input cannot smuggle a script into your output. This is always on and independent of options.sanitizer, which governs URL schemes alone.

Removed before rendering:

  • Inline event handlers: onclick, onerror, onload, and any other on* attribute.
  • URL attributes carrying a javascript:, vbscript:, or non-image data: scheme, in href, src, action, formaction, poster, cite, background, data, longdesc, and xlink:href. Schemes hidden behind HTML entities such as java&#9;script: or a semicolon-less &#106avascript: are decoded and caught too.
  • The iframe srcdoc attribute.

Kept: safe attributes with their original formatting, data:image URLs, and event handlers you pass as expressions to your own components (<MyButton onClick={fn} />) along with bare boolean props on them (<MyButton onClick />). One caveat: data:image/svg+xml is allowed but can execute script when opened as a top-level navigation, so treat SVG data URLs from untrusted sources with care.

Dangerous tag names (script, iframe, style, and similar) are escaped separately by the tagfilter option, which is on by default.

options.slugify

By default, a lightweight deburring function is used to generate an HTML id from each heading's plain text content (formatting markers, link destinations, and image alt text are omitted; footnote markers contribute their display text). When more than one heading produces the same id, a numeric suffix is added automatically (foo, foo-1, foo-2) so each id stays unique within that parse. You can override slug generation by passing a function to options.slugify; the function receives that plain text content, and uniqueness is still applied to whatever it returns. This is helpful when you are using non-alphanumeric characters (e.g. Chinese or Japanese characters) in headings. For example:

<Markdown options={{ slugify: str => str }}># δΈ­ζ–‡</Markdown>
compiler('# δΈ­ζ–‡', { slugify: str => str })

The original function is available as a library export called slugify.

options.wrapper

When there are multiple children to be rendered, the compiler will wrap the output in a div by default. You can override this default by setting the wrapper option to either a string (React Element) or a component.

const str = '# Heck Yes\n\nThis is great!'

<Markdown options={{ wrapper: 'article' }}>{str}</Markdown>

compiler(str, { wrapper: 'article' })
Other useful recipes

To get an array of children back without a wrapper, set wrapper to null. This is particularly useful when using compiler(…) directly.

compiler('One\n\nTwo\n\nThree', { wrapper: null })[
  /** Returns */ ((<p>One</p>), (<p>Two</p>), (<p>Three</p>))
]

To render children at the same DOM level as <Markdown> with no HTML wrapper, set wrapper to React.Fragment. This will still wrap your children in a React node for the purposes of rendering, but the wrapper element won't show up in the DOM.

options.wrapperProps

Props to apply to the wrapper element when wrapper is used.

<Markdown
  options={{
    wrapper: 'article',
    wrapperProps: { className: 'post', 'data-testid': 'markdown-content' },
  }}
>
  # Hello World
</Markdown>

Syntax highlighting

When using fenced code blocks with language annotation, that language will be added to the <code> element as class="language-${language}". The JSX renderers (React, React Native, Solid, Vue) add a legacy lang-${language} alongside it. For best results, you can use options.overrides to provide an appropriate syntax highlighting integration like this one using highlight.js:

<!-- Add the following tags to your page <head> to automatically load hljs and styles: -->
<link
  rel="stylesheet"
  href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/obsidian.min.css"
/>

<script
  crossorigin
  src="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js"
></script>
import { Markdown, RuleType } from 'markdown-to-jsx'

const mdContainingFencedCodeBlock = '```js\nconsole.log("Hello world!");\n```\n'

function App() {
  return (
    <Markdown
      children={mdContainingFencedCodeBlock}
      options={{
        overrides: {
          code: SyntaxHighlightedCode,
        },
      }}
    />
  )
}

function SyntaxHighlightedCode(props) {
  const ref = React.useRef<HTMLElement | null>(null)

  React.useEffect(() => {
    if (ref.current && props.className?.includes('lang-') && window.hljs) {
      window.hljs.highlightElement(ref.current)

      // hljs won't reprocess the element unless this attribute is removed
      ref.current.removeAttribute('data-highlighted')
    }
  }, [props.className, props.children])

  return <code {...props} ref={ref} />
}

Handling shortcodes

For Slack-style messaging with arbitrary shortcodes like :smile:, you can use options.renderRule to hook into the plain text rendering and adjust things to your liking, for example:

import Markdown, { RuleType } from 'markdown-to-jsx'

const shortcodeMap = {
  smile: 'πŸ™‚',
}

const detector = /(:[^:]+:)/g

const replaceEmoji = (text: string): React.ReactNode => {
  return text.split(detector).map((part, index) => {
    if (part.startsWith(':') && part.endsWith(':')) {
      const shortcode = part.slice(1, -1)

      return <span key={index}>{shortcodeMap[shortcode] || part}</span>
    }

    return part
  })
}

function Example() {
  return (
    <Markdown
      options={{
        renderRule(next, node) {
          if (node.type === RuleType.text && detector.test(node.text)) {
            return replaceEmoji(node.text)
          }

          return next()
        },
      }}
    >
      {`On a beautiful summer day, all I want to do is :smile:.`}
    </Markdown>
  )
}

When you use options.renderRule, any React-renderable JSX may be returned including images and GIFs. Ensure you benchmark your solution as the text rule is one of the hottest paths in the system!

Streaming Markdown

When rendering markdown content that arrives incrementally (e.g., from an AI/LLM API, WebSocket, or Server-Sent Events), you may notice raw markdown syntax briefly appearing before it renders properly. This happens because incomplete syntax like **bold text or <CustomComponent>partial content gets rendered as text before the closing delimiter arrives.

The optimizeForStreaming option solves this by detecting incomplete markdown structures and holding them back until the content is complete (returning null on React and React Native, an empty string on HTML). It works across every renderer:

import Markdown from 'markdown-to-jsx/react'

function StreamingMarkdown({ content }) {
  return <Markdown options={{ optimizeForStreaming: true }}>{content}</Markdown>
}

LLM / AI chatbot integration:

A common pattern is rendering streamed responses from LLM APIs (OpenAI, Anthropic, etc.) where tokens arrive one at a time. Without optimizeForStreaming, users see distracting flashes of raw markdown syntax between each token. With it enabled, incomplete structures are suppressed until the closing delimiter arrives, producing a smooth reading experience:

import Markdown from 'markdown-to-jsx/react'
import { useState, useEffect } from 'react'

function ChatMessage({ stream }) {
  const [content, setContent] = useState('')

  useEffect(() => {
    // Accumulate tokens from the LLM stream
    stream.on('token', token => setContent(prev => prev + token))
  }, [stream])

  return <Markdown options={{ optimizeForStreaming: true }}>{content}</Markdown>
}

What it suppresses:

  • Unclosed HTML tags (<div>content without </div>)
  • Incomplete tag syntax (<div attr="value without closing >)
  • Unclosed HTML comments (<!-- comment without -->)
  • Unclosed inline code (`code without closing backtick)
  • Unclosed bold/italic (**text or *text without closing)
  • Unclosed strikethrough (~~text without closing ~~)
  • Unclosed links ([text](https://github.com/quantizor/markdown-to-jsx/blob/HEAD/url without closing ))
  • Incomplete tables (a header or divider row before the first data row); once the table renders, it stays put as further rows stream in, so it never flashes raw pipes or flickers between rows

What renders normally (content visible as it streams):

  • Fenced code blocks - content is displayed as it arrives, waiting for closing fence

Usage with Preact

Everything will work just fine! Simply Alias react to preact/compat like you probably already are doing.

AST Anatomy

The Abstract Syntax Tree (AST) is a structured representation of parsed markdown. Each node in the AST has a type property that identifies its kind, and type-specific properties.

Important: The first node in the AST is typically a RuleType.refCollection node that contains all reference definitions found in the document, including footnotes (stored with keys prefixed with ^). This node is skipped during rendering but is useful for accessing reference data. Footnotes are automatically extracted from the refCollection and rendered in a <footer> element by both compiler() and astToJSX().

Node Types

The AST consists of the following node types (use RuleType to check node types):

Block-level nodes:

  • RuleType.heading - Headings (# Heading)

    { type: RuleType.heading, level: 1, id: "heading", children: [...] }
    
  • RuleType.paragraph - Paragraphs

    { type: RuleType.paragraph, children: [...] }
    
  • RuleType.codeBlock - Fenced code blocks (```)

    { type: RuleType.codeBlock, lang: "javascript", text: "code content", attrs?: { "data-line": "1" } }
    
  • RuleType.blockQuote - Blockquotes (>)

    { type: RuleType.blockQuote, children: [...], alert?: "note" }
    
  • RuleType.orderedList / RuleType.unorderedList - Lists

    { type: RuleType.orderedList, items: [[...]], start?: 1 }
    { type: RuleType.unorderedList, items: [[...]] }
    
  • RuleType.table - Tables

    { type: RuleType.table, header: [...], cells: [[...]], align: [...] }
    
  • RuleType.htmlBlock - HTML blocks and JSX components

    { type: RuleType.htmlBlock, tag: "div", attrs?: Record<string, any>, children?: ASTNode[] }
    

    Note: PascalCase JSX components and hyphenated custom elements nest their children when blank lines appear between the opening and closing tags, instead of leaking later siblings. Known HTML block tags (div, figure, and the rest of the CommonMark block set) behave the same way. Lowercase unknown tags still follow CommonMark Type 7 and stop at the first blank line.

    HTML Block Parsing (v9.2+): HTML blocks are always fully parsed into the children property. The renderRule callback can access the fully parsed AST in children for all HTML blocks.

Inline nodes:

  • RuleType.text - Plain text
    { type: RuleType.text, text: "Hello world" }
    
  • RuleType.textFormatted - Bold, italic, etc.
    { type: RuleType.textFormatted, tag: "strong", children: [...] }
    
  • RuleType.codeInline - Inline code (`)
    { type: RuleType.codeInline, text: "code" }
    
  • RuleType.link - Links
    { type: RuleType.link, target: "https://example.com", title?: "Link title", children: [...] }
    
  • RuleType.image - Images
    { type: RuleType.image, target: "image.png", alt?: "description", title?: "Image title" }
    

Other nodes:

  • RuleType.breakLine - Hard line breaks ( )
  • RuleType.breakThematic - Horizontal rules (---)
  • RuleType.gfmTask - GFM task list items (- [ ])
    { type: RuleType.gfmTask, completed: false }
    
  • RuleType.ref - Reference definition node (not rendered, stored in refCollection)
  • RuleType.refCollection - Reference definitions collection (appears at AST root, includes footnotes with ^ prefix)
    { type: RuleType.refCollection, refs: { "label": { target: "url", title: "title" } } }
    
  • RuleType.footnote - Footnote definition node (not rendered, stored in refCollection)
  • RuleType.footnoteReference - Footnote reference ([^identifier])
    { type: RuleType.footnoteReference, target: "#fn-identifier", text: "1" }
    
  • RuleType.frontmatter - YAML frontmatter blocks
    { type: RuleType.frontmatter, text: "---\ntitle: My Title\n---" }
    
  • RuleType.htmlComment - HTML comment nodes
    { type: RuleType.htmlComment, text: "comment text" }
    
  • RuleType.htmlSelfClosing - Self-closing HTML tags
    { type: RuleType.htmlSelfClosing, tag: "img", attrs?: { src: "image.png" } }
    

JSX Prop Parsing (v9.1+):

The parser intelligently parses JSX prop values:

  • Arrays/objects are parsed via JSON.parse(): rows={[["a", "b"]]} β†’ attrs.rows = [["a", "b"]]
  • Functions are kept as strings for security: onClick={() => ...} β†’ attrs.onClick = "() => ..."
  • Booleans are parsed: enabled={true} β†’ attrs.enabled = true

Example AST Structure

import { parser, RuleType } from 'markdown-to-jsx'

const ast = parser(`# Hello World

This is a **paragraph** with [a link](https://example.com).

[linkref]: https://example.com

```javascript
console.log('code')
```

`)

// AST structure:
[
  // Reference collection (first node, if references exist)
  {
    type: RuleType.refCollection,
    refs: {
      linkref: { target: 'https://example.com', title: undefined },
    },
  },
  {
    type: RuleType.heading,
    level: 1,
    id: 'hello-world',
    children: [{ type: RuleType.text, text: 'Hello World' }],
  },
  {
    type: RuleType.paragraph,
    children: [
      { type: RuleType.text, text: 'This is a ' },
      {
        type: RuleType.textFormatted,
        tag: 'strong',
        children: [{ type: RuleType.text, text: 'paragraph' }],
      },
      { type: RuleType.text, text: ' with ' },
      {
        type: RuleType.link,
        target: 'https://example.com',
        children: [{ type: RuleType.text, text: 'a link' }],
      },
      { type: RuleType.text, text: '.' },
    ],
  },
  {
    type: RuleType.codeBlock,
    lang: 'javascript',
    text: "console.log('code')",
  },
]

Type Checking

Use the RuleType enum to identify AST nodes:

import { RuleType } from 'markdown-to-jsx'

if (node.type === RuleType.heading) {
  const heading = node as MarkdownToJSX.HeadingNode
  console.log(`Heading level ${heading.level}: ${heading.id}`)
}

When to use compiler vs parser vs <Markdown>:

  • Use <Markdown> when you need a simple React component that renders markdown to JSX.
  • Use compiler when you need React JSX output from markdown (the component uses this internally).
  • Use parser + astToJSX when you need the AST for custom processing before rendering to JSX, or just the AST itself.

Gotchas

JSX prop parsing (v9.1+): Arrays and objects in JSX props are automatically parsed:

// In markdown:
<Table
  columns={['Name', 'Age']}
  data={[
    ['Alice', 30],
    ['Bob', 25],
  ]}
/>

// In your component (v9.1+):
const Table = ({ columns, data, ...props }) => {
  // columns is already an array: ["Name", "Age"]
  // data is already an array: [["Alice", 30], ["Bob", 25]]
  // No JSON.parse needed!
}

// For backwards compatibility, check types:
const Table = ({ columns, data, ...props }) => {
  const parsedColumns =
    typeof columns === 'string' ? JSON.parse(columns) : columns
  const parsedData = typeof data === 'string' ? JSON.parse(data) : data
}

Function props are kept as strings for security. Use renderRule for case-by-case handling, or see evalUnserializableExpressions for opt-in eval.

HTML indentation: Leading whitespace in HTML blocks is auto-trimmed based on the first line's indentation to avoid markdown syntax conflicts.

Code in HTML: Don't put code directly in HTML divs. Use fenced code blocks instead:

<div>
```js
var code = here();
```
</div>

Changelog

See Github Releases.

Donate

Like this library? It's developed entirely on a volunteer basis; chip in a few bucks if you can via the Sponsor link!