@uiw/react-markdown-preview, react-markdown, and react-markdown-editor-lite are React-focused libraries for handling Markdown content, but they serve distinct roles. react-markdown is a lightweight, security-first renderer that converts Markdown strings into React elements using a syntax tree. @uiw/react-markdown-preview builds on top of react-markdown to provide a ready-to-use preview component with built-in syntax highlighting and common extensions like tables and task lists. react-markdown-editor-lite is a full-featured WYSIWYG Markdown editor that includes both an editing pane and a live preview, targeting users who need an integrated authoring experience.
When working with Markdown in React apps, developers often face a key decision: do you just need to render safe HTML from Markdown, show a polished preview, or provide a full editing experience? The three packages — react-markdown, @uiw/react-markdown-preview, and react-markdown-editor-lite — each solve one of these problems, but with very different trade-offs in scope, security, and bundle impact. Let’s break them down.
react-markdown: Pure, Secure RenderingThis library focuses exclusively on converting Markdown strings into React elements. It uses the remark ecosystem under the hood but avoids dangerouslySetInnerHTML, making it inherently resistant to XSS attacks. You pass it a Markdown string, and it outputs a tree of React components.
import ReactMarkdown from 'react-markdown';
function App() {
const markdown = '# Hello\nThis is **bold**.';
return <ReactMarkdown>{markdown}</ReactMarkdown>;
}
It does not include syntax highlighting, table styling, or task list support by default — you add those via plugins or custom components.
@uiw/react-markdown-preview: Batteries-Included PreviewBuilt directly on react-markdown, this package provides a ready-to-use preview component with sensible defaults for common Markdown extensions. It enables GitHub-flavored Markdown (GFM), adds Prism.js-based syntax highlighting, and styles tables and task lists out of the box.
import MarkdownPreview from '@uiw/react-markdown-preview';
function App() {
const markdown = '```js\nconsole.log("hello");\n```\n\n- [x] Task done';
return <MarkdownPreview source={markdown} />;
}
Note the source prop instead of children — this is a small but important API difference from react-markdown.
react-markdown-editor-lite: Full WYSIWYG EditorThis is not just a renderer — it’s a complete Markdown editor with dual panes: one for writing, one for live preview. It includes a toolbar, image upload hooks, and undo/redo functionality. Think of it as a lightweight alternative to Draft.js or TipTap, but Markdown-specific.
import MdEditor from 'react-markdown-editor-lite';
import 'react-markdown-editor-lite/lib/index.css';
function App() {
const handleEditorChange = ({ text }) => {
console.log(text);
};
return (
<MdEditor
value="# Edit me"
style={{ height: '500px' }}
onChange={handleEditorChange}
/>
);
}
You must import its CSS file separately, which is typical for component libraries with complex UI.
All three packages aim to prevent XSS, but their approaches differ.
react-markdown is the most secure by design. Since it never uses innerHTML, even malicious input like <script>alert(1)</script> is rendered as plain text. You can further lock it down by restricting allowed HTML tags via the allowedElements prop.
// react-markdown: Explicitly block all HTML
<ReactMarkdown allowedElements={[]} unwrapDisallowed={true}>
{userInput}
</ReactMarkdown>
@uiw/react-markdown-preview inherits this safety because it wraps react-markdown. However, because it enables GFM by default (which includes limited HTML like <br>), you should still treat input cautiously. It doesn’t expose the full react-markdown props by default, so tightening security requires using the components or remarkPlugins props directly.
// @uiw/react-markdown-preview: Disable raw HTML
import gfm from 'remark-gfm';
<MarkdownPreview
source={userInput}
remarkPlugins={[[gfm, { singleTilde: false }]]}
skipHtml={true}
/>
react-markdown-editor-lite assumes the content is being authored by a trusted user (e.g., an admin), so it focuses less on sanitization. While it doesn’t execute scripts during preview, it doesn’t sanitize output by default. If you accept editor output from untrusted users, you must sanitize it server-side or with a library like DOMPurify before storing or re-rendering.
Need to render links with react-router’s <Link>? Or style code blocks differently? Here’s how each package handles customization.
react-markdown gives you full control via the components prop:
import { Link } from 'react-router-dom';
<ReactMarkdown
components={{
a: ({ node, ...props }) => <Link to={props.href} {...props} />
}}
>
{markdown}
</ReactMarkdown>
@uiw/react-markdown-preview also supports components, but you lose some of its built-in styling if you override elements like code or pre without preserving Prism classes:
<MarkdownPreview
source={markdown}
components={{
code({ node, inline, className, children, ...props }) {
if (inline) return <code {...props}>{children}</code>;
return (
<pre className={className} {...props}>
<code>{children}</code>
</pre>
);
}
}}
/>
react-markdown-editor-lite allows preview customization through the view prop, but it’s more limited. You typically replace the entire preview renderer:
<MdEditor
value={markdown}
view={{
menu: true,
md: true,
html: true,
fullScreen: false,
hideMenu: false
}}
// To customize preview, you'd need to fork or use dangerouslySetInnerHTML
/>
For deep preview customization in the editor, you’re better off using react-markdown inside a custom preview pane.
react-markdown requires manual setup:
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { dark } from 'react-syntax-highlighter/dist/esm/styles/prism';
<ReactMarkdown
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter
style={dark}
language={match[1]}
PreTag="div"
{...props}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
);
}
}}
>
{markdown}
</ReactMarkdown>
@uiw/react-markdown-preview includes Prism automatically. Just ensure you import its CSS:
import '@uiw/react-markdown-preview/lib/styles/github.css';
<MarkdownPreview source={markdown} />
react-markdown-editor-lite uses highlight.js by default and bundles its own themes. You can switch themes via props but can’t easily swap the highlighter engine.
<MdEditor
value={markdown}
config={{
view: { menu: true, md: true, html: true },
imageUrl: '/upload'
}}
style={{ height: '400px' }}
/>
You’re building a forum where users post Markdown comments. Security and simplicity are key.
react-markdownYou need to display styled, syntax-highlighted docs (like READMEs) with tables and task lists.
@uiw/react-markdown-previewAuthors need to write and preview posts in a single interface with image uploads.
react-markdown-editor-litereact-markdown-editor-lite for read-only scenarios — it ships editor logic, toolbar assets, and state management you won’t use.@uiw/react-markdown-preview if you need to deeply customize parsing (e.g., custom directives) — you’ll fight its opinionated defaults.react-markdown if you want syntax highlighting or GFM without wiring up plugins — you’ll spend time reinventing what @uiw provides out of the box.Think about your user’s role:
@uiw/react-markdown-preview for rich previews, or react-markdown for maximum control.react-markdown-editor-lite for a complete editing environment.And always remember: if the Markdown comes from an untrusted source, lean on react-markdown’s security model — never trust client-side sanitization alone.
Choose @uiw/react-markdown-preview when you need a zero-config Markdown preview component with syntax highlighting, GitHub-flavored Markdown support (tables, task lists), and minimal setup. It’s ideal for documentation viewers, comment previews, or any read-only display where you want rich rendering without building your own pipeline. Avoid it if you require fine-grained control over the parsing or rendering pipeline, as it abstracts away the underlying react-markdown configuration.
Choose react-markdown when you need maximum control, security, and composability in rendering Markdown. It sanitizes content by default, supports custom components for every node type, and integrates cleanly with remark plugins. This package is best for applications where Markdown is user-provided (e.g., forums, CMS backends) and you must enforce strict XSS protection or customize rendering logic per element type.
Choose react-markdown-editor-lite when your application requires a full Markdown authoring interface with live preview, toolbar, and image upload support. It’s designed for content creators who need an intuitive editor similar to GitHub’s or Stack Overflow’s. Avoid it if you only need rendering (not editing), as it bundles unnecessary UI and state management overhead for read-only use cases.
React component preview markdown text in web browser. The minimal amount of CSS to replicate the GitHub Markdown style. The current document website is converted using this react component.
@v4$ npm install @uiw/react-markdown-preview --save
import React from 'react';
import MarkdownPreview from '@uiw/react-markdown-preview';
const source = `
## MarkdownPreview
> todo: React component preview markdown text.
`;
export default function Demo() {
return (
<MarkdownPreview source={source} style={{ padding: 16 }} />
)
}
import React from 'react';
import MarkdownPreview from '@uiw/react-markdown-preview';
const source = `
## MarkdownPreview
## Header 2
### Header 3
`;
export default function Demo() {
return (
<MarkdownPreview
source={source}
style={{ padding: 16 }}
rehypeRewrite={(node, index, parent) => {
if (node.tagName === "a" && parent && /^h(1|2|3|4|5|6)/.test(parent.tagName)) {
parent.children = parent.children.slice(1)
}
}}
/>
);
}
syntax: ```jsx {1,4-5}
import React from 'react';
import MarkdownPreview from '@uiw/react-markdown-preview';
const source = `
\`\`\`js {2}
function () {
console.log('hello hello hello hello hello hello hello hello hello hello hello hello hello hello hello hello')
}
\`\`\`
\`\`\`js {2}
function () {
console.log('hello ')
}
\`\`\`
`;
export default function Demo() {
return (
<MarkdownPreview
source={source}
style={{ padding: 16 }}
rehypeRewrite={(node, index, parent) => {
if (node.tagName === "a" && parent && /^h(1|2|3|4|5|6)/.test(parent.tagName)) {
parent.children = parent.children.slice(1)
}
}}
/>
);
}
syntax: ```jsx showLineNumbers {1,4-5}
import React from 'react';
import MarkdownPreview from '@uiw/react-markdown-preview';
const source = `
\`\`\`js showLineNumbers
function () {
console.log('hello hello hello hello hello hello hello hello hello hello hello hello hello hello hello hello')
}
\`\`\`
\`\`\`js showLineNumbers {2}
function () {
console.log('hello ')
}
\`\`\`
`;
export default function Demo() {
return (
<MarkdownPreview
source={source}
style={{ padding: 16 }}
rehypeRewrite={(node, index, parent) => {
if (node.tagName === "a" && parent && /^h(1|2|3|4|5|6)/.test(parent.tagName)) {
parent.children = parent.children.slice(1)
}
}}
/>
);
}
import React from 'react';
import MarkdownPreview from '@uiw/react-markdown-preview';
const source = `
\`\`\`js
function () {
console.log('hello hello hello hello hello hello hello hello hello hello hello hello hello hello hello hello')
}
\`\`\`
\`\`\`js
function () {
console.log('hello ')
}
\`\`\`
`;
export default function Demo() {
return (
<MarkdownPreview source={source} style={{ padding: 16 }} />
);
}
Use @uiw/react-markdown-preview/common to keep syntax highlighting enabled with the rehype-prism-plus/common language subset. This is a middle ground between the default all-language build and nohighlight.
import React from 'react';
import MarkdownPreview from '@uiw/react-markdown-preview/common';
const source = `
\`\`\`js
function greet(name) {
console.log('hello', name);
}
\`\`\`
`;
export default function Demo() {
return (
<MarkdownPreview source={source} style={{ padding: 16 }} />
);
}
The following example can help you exclude code highlighting code from being included in the bundle. @uiw/react-markdown-preview/nohighlight component does not contain the rehype-prism-plus code highlighting package, showLineNumbers and highlight line functions will no longer work. (#586)
import React from 'react';
import MarkdownPreview from '@uiw/react-markdown-preview/nohighlight';
const source = `
\`\`\`js
function () {
console.log('hello hello hello hello hello hello hello hello hello hello hello hello hello hello hello hello')
}
\`\`\`
\`\`\`js
function () {
console.log('hello ')
}
\`\`\`
`;
export default function Demo() {
return (
<MarkdownPreview
source={source}
style={{ padding: 16 }}
rehypeRewrite={(node, index, parent) => {
if (node.tagName === "a" && parent && /^h(1|2|3|4|5|6)/.test(parent.tagName)) {
parent.children = parent.children.slice(1)
}
}}
/>
);
}
Ignore content display via HTML comments, Shown in GitHub readme, excluded in HTML.
import React from 'react';
import MarkdownPreview from '@uiw/react-markdown-preview';
const source = `
<!--rehype:ignore:start-->
Content ignored
<!--rehype:ignore:end-->
Some content is ignored, please check the source code
`;
export default function Demo() {
return (
<MarkdownPreview
source={source}
style={{ padding: 16 }}
rehypeRewrite={(node, index, parent) => {
if (node.tagName === "a" && parent && /^h(1|2|3|4|5|6)/.test(parent.tagName)) {
parent.children = parent.children.slice(1)
}
}}
/>
);
}
<!--rehype:ignore:start-->Ignored content<!--rehype:ignore:end-->
KaTeX is a fast, easy-to-use JavaScript library for TeX math rendering on the web, We perform math rendering through KaTeX.
npm install katex
import React from 'react';
import MarkdownPreview from '@uiw/react-markdown-preview';
import { getCodeString } from 'rehype-rewrite';
import katex from 'katex';
import 'katex/dist/katex.css';
const source = `This is to display the
\`\$\$\c = \\pm\\sqrt{a^2 + b^2}\$\$\`
in one line
\`\`\`KaTeX
c = \\pm\\sqrt{a^2 + b^2}
\`\`\`
`;
export default function Demo() {
const [value, setValue] = React.useState(source);
return (
<MarkdownPreview
source={source}
style={{ padding: 16 }}
components={{
code: ({ children = [], className, ...props }) => {
if (typeof children === 'string' && /^\$\$(.*)\$\$/.test(children)) {
const html = katex.renderToString(children.replace(/^\$\$(.*)\$\$/, '$1'), {
throwOnError: false,
});
return <code dangerouslySetInnerHTML={{ __html: html }} style={{ background: 'transparent' }} />;
}
const code = props.node && props.node.children ? getCodeString(props.node.children) : children;
if (
typeof code === 'string' &&
typeof className === 'string' &&
/^language-katex/.test(className.toLocaleLowerCase())
) {
const html = katex.renderToString(code, {
throwOnError: false,
});
return <code style={{ fontSize: '150%' }} dangerouslySetInnerHTML={{ __html: html }} />;
}
return <code className={String(className)}>{children}</code>;
},
}}
/>
);
}
Using mermaid to generation of diagram and flowchart from text in a similar manner as markdown
import React, { useState, useRef, useEffect, Fragment, useCallback } from "react";
import MarkdownPreview from '@uiw/react-markdown-preview';
import { getCodeString } from 'rehype-rewrite';
import mermaid from "mermaid";
const randomid = () => parseInt(String(Math.random() * 1e15), 10).toString(36);
const Code = ({ inline, children = [], className, ...props }) => {
const demoid = useRef(`dome${randomid()}`);
const [container, setContainer] = useState(null);
const isMermaid = className && /^language-mermaid/.test(className.toLocaleLowerCase());
const code = props.node && props.node.children ? getCodeString(props.node.children) : children[0] || '';
const reRender = async () => {
if (container && isMermaid) {
try {
const str = await mermaid.render(demoid.current, code);
container.innerHTML = str.svg;
} catch (error) {
container.innerHTML = error;
}
}
}
useEffect(() => {
reRender()
}, [container, isMermaid, code, demoid]);
const refElement = useCallback((node) => {
if (node !== null) {
setContainer(node);
}
}, []);
if (isMermaid) {
return (
<Fragment>
<code id={demoid.current} style={{ display: "none" }} />
<code ref={refElement} data-name="mermaid" />
</Fragment>
);
}
return <code>{children}</code>;
};
const source = `The following are some examples of the diagrams, charts and graphs that can be made using Mermaid and the Markdown-inspired text specific to it.
\`\`\`mermaid
graph TD
A[Hard] -->|Text| B(Round)
B --> C{Decision}
C -->|One| D[Result 1]
C -->|Two| E[Result 2]
\`\`\`
\`\`\`mermaid
sequenceDiagram
Alice->>John: Hello John, how are you?
loop Healthcheck
John->>John: Fight against hypochondria
end
Note right of John: Rational thoughts!
John-->>Alice: Great!
John->>Bob: How about you?
Bob-->>John: Jolly good!
\`\`\`
`;
// const source = `
// \`\`\`mermaid
// graph TD;
// A-->B;
// A-->C;
// B-->D;
// C-->D;
// \`\`\`
// `;
export default function Demo() {
return (
<MarkdownPreview
source={source}
style={{ padding: 16 }}
components={{
code: Code
}}
/>
);
}
Please note markdown needs to be sanitized if you do not completely trust your authors. Otherwise, your app is vulnerable to XSS. This can be achieved by adding rehype-sanitize as a plugin.
import React from 'react';
import rehypeSanitize from "rehype-sanitize";
import MarkdownPreview from '@uiw/react-markdown-preview';
const source = `
## MarkdownPreview
**Hello world!!!** <IFRAME SRC=\"javascript:javascript:alert(window.origin);\"></IFRAME>
<!-- test --> 123
<!-- test --> 456 <!-- test -->
`;
const rehypePlugins = [rehypeSanitize];
export default function Demo() {
return (
<MarkdownPreview source={source} rehypePlugins={rehypePlugins} style={{ padding: 16 }} />
)
}
import { ReactMarkdownProps } from 'react-markdown';
import { RehypeRewriteOptions } from 'rehype-rewrite';
type MarkdownPreviewProps = {
prefixCls?: string;
className?: string;
source?: string;
disableCopy?: boolean;
style?: React.CSSProperties;
pluginsFilter?: (type: 'rehype' | 'remark', plugin: PluggableList) => PluggableList;
wrapperElement?: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
'data-color-mode'?: 'light' | 'dark';
};
onScroll?: (e: React.UIEvent<HTMLDivElement>) => void;
onMouseOver?: (e: React.MouseEvent<HTMLDivElement>) => void;
rehypeRewrite?: RehypeRewriteOptions['rewrite'];
} & ReactMarkdownProps;
source (string, default: '')className (string?)div with this class nameThis ReactMarkdownProps details. Upgrade react-markdown v9
children (string, default: '')className (string?)div with this class nameskipHtml (boolean, default: falsetrue )allowElement ((element, index, parent) => boolean?, optional)allowedElements / disallowedElements is used first!remarkPlugins (Array.<Plugin>, default: [])rehypePlugins (Array.<Plugin>, default: [])[!NOTE]
urlTransformThe transformImageUri and transformLinkUri were removed.
Having two functions is a bit much, particularly because there are more URLs
you might want to change (or which might be unsafe so we make them safe).
And their name and APIs were a bit weird.
You can use the new urlTransform prop instead to change all your URLs.
linkTargetThe linkTarget option was removed; you should likely not set targets.
If you want to, use
rehype-external-links.
includeElementIndexThe includeElementIndex option was removed, so index is never passed to
components.
Write a plugin to pass index:
import {visit} from 'unist-util-visit'
function rehypePluginAddingIndex() {
/**
* @param {import('hast').Root} tree
* @returns {undefined}
*/
return function (tree) {
visit(tree, function (node, index) {
if (node.type === 'element' && typeof index === 'number') {
node.properties.index = index
}
})
}
}
rawSourcePosThe rawSourcePos option was removed, so sourcePos is never passed to
components.
All components are passed node, so you can get node.position from them.
sourcePosThe sourcePos option was removed, so data-sourcepos is never passed to
elements.
Write a plugin to pass index:
import {stringifyPosition} from 'unist-util-stringify-position'
import {visit} from 'unist-util-visit'
function rehypePluginAddingIndex() {
/**
* @param {import('hast').Root} tree
* @returns {undefined}
*/
return function (tree) {
visit(tree, function (node) {
if (node.type === 'element') {
node.properties.dataSourcepos = stringifyPosition(node.position)
}
})
}
}
When overwriting components, these props are no longer passed:
inline on code
— create a plugin or use pre for the blocklevel on h1, h2, h3, h4, h5, h6
— check node.tagName insteadchecked on li
— check task-list-item class or check props.childrenindex on li
— create a pluginordered on li
— create a plugin or check the parentdepth on ol, ul
— create a pluginordered on ol, ul
— check node.tagName insteadisHeader on td, th
— check node.tagName insteadisHeader on tr
— create a plugin or check childrenUse HTML comments <!--rehype:xxx--> to let Markdown support style customization.
## Title
<!--rehype:style=display: flex; height: 230px; align-items: center; justify-content: center; font-size: 38px;-->
Markdown Supports **Style**<!--rehype:style=color: red;-->
Here is a simple footnote[^1]. With some additional text after it.
[^1]: My reference.
# Hello World
<!--rehype:ignore:start-->Hello World<!--rehype:ignore:end-->
Good!
Output:
<h1>Hello World</h1>
<p>Good!</p>
import React from 'react';
import MarkdownPreview from '@uiw/react-markdown-preview';
const source = `>
>
> [!NOTE]
> Useful information that users should know, even when skimming content.
> [!TIP]
> Helpful advice for doing things better or more easily.
> [!IMPORTANT]
> Key information users need to know to achieve their goal.
> [!WARNING]
> Urgent info that needs immediate user attention to avoid problems.
> [!CAUTION]
> Advises about risks or negative outcomes of certain actions.
`;
export default function Demo() {
return (
<MarkdownPreview source={source} style={{ padding: 16 }} />
)
}
By default, the dark-mode is automatically switched according to the system. If you need to switch manually, just set the data-color-mode="dark" parameter for body.
<html data-color-mode="dark">
document.documentElement.setAttribute('data-color-mode', 'dark')
document.documentElement.setAttribute('data-color-mode', 'light')
Inherit custom color variables by adding .wmde-markdown-var selector.
const Demo = () => {
return (
<div>
<div className="wmde-markdown-var"> </div>
<MarkdownPreview source="Hello World!" />
</div>
)
}
Set the light theme.
<MarkdownPreview
source="Hello World!"
wrapperElement={{
+ "data-color-mode": "light"
}}
/>
Runs the project in development mode.
# Step 1, run first,
# listen to the component compile and output the .js file
# listen for compilation output type .d.ts file
# listen to the component compile and output the .css file
npm run start
# Step 2, development mode, listen to compile preview website instance
npm run doc
Builds the app for production to the build folder.
npm run build
The build is minified and the filenames include the hashes. Your app is ready to be deployed!
If you need more features-rich Markdown Editor, you can use @uiwjs/react-markdown-editor
As always, thanks to our amazing contributors!
Made with action-contributors.
Licensed under the MIT License.