✨ Strapi MCP is now Generally Available - let your agents manage your Strapi content ✨

Ecosystem14 min read

5 Markdown Editors for React Compared by Bundle Size, Maintenance, and Integration

December 9, 2025Updated on September 14, 2026
5 Markdown Editors for React

Choosing a Markdown editor for a React app can go wrong when a package looks healthy on npm but has not shipped a release in years, or when it works in a Vite demo and then throws errors under server-side rendering in Next.js.

This comparison covers five Markdown editors for React (@uiw/react-md-editor, MDXEditor, Milkdown, react-markdown-editor-lite, and react-simplemde-editor) using version-stamped bundle sizes, release dates, open-issue counts, and SSR guidance taken from npm, GitHub, Bundlephobia, and each project's documentation. It is written for full-stack developers building documentation sites, blogs, or CMS admin interfaces, and it ends with how to wire any of these editors into Strapi 5 as a custom field.

One distinction before the comparison: react-markdown is a renderer, and its README states it is "not an editor." You will likely use it on the frontend to display what one of the editors below produces. If you need a refresher on the syntax itself, start with the guide on Markdown formatting.

In brief:

  • @uiw/react-md-editor 4.1.2 measures 5.9 kB gzipped JavaScript plus 6 kB of CSS and ships TypeScript declarations, but its stylesheet uses !important rules and the repository has open accessibility and cursor-position issues.
  • MDXEditor 4.2.4 dropped to 564 kB gzipped from 851.1 kB in 3.45.1 and declares React 18 and 19 as peers, though the GenericJsxEditor discussion on inline rendering remains open.
  • Milkdown's Crepe package now provides a prebuilt toolbar, tables, and LaTeX, but the React integration still has no native props for value or onChange.
  • react-markdown-editor-lite shipped 1.4.2 in January 2026; react-simplemde-editor has not been updated since October 2022 and pins EasyMDE below 3.

What to Check Before Choosing a React Markdown Editor

The first decision is the editing model. @uiw/react-md-editor and react-markdown-editor-lite are source editors: you type Markdown in a textarea and see a rendered preview beside it. MDXEditor and Milkdown are inline WYSIWYG editors where formatting appears as you type, which means they parse Markdown into an internal document model and serialize it back out.

The second is Markdown flavor. CommonMark is the formal specification. GitHub Flavored Markdown is "a strict superset of CommonMark" that adds tables, task lists, strikethrough, and autolinks. MDX combines Markdown with JSX and JavaScript expressions, and its documentation warns: "Do not let random people from the internet write MDX."

The third is storage. Markdown is portable plain text, but it cannot represent an image gallery or a call-to-action block the way JSON-based block formats can, a trade-off covered in Strapi's structured content guide. The editors in this comparison are designed to accept or produce Markdown strings, so using that output on the frontend requires a Markdown-to-React rendering pipeline.

@uiw/react-md-editor: Textarea Editor With Live Preview

@uiw/react-md-editor builds on a native textarea rather than CodeMirror or Monaco, and its JavaScript footprint is small. Version 4.1.2 was published on August 21, 2026, and the package records about 547,000 weekly downloads for September 5 to 11, 2026. The library is written in TypeScript and ships its own .d.ts files.

Bundlephobia measures 4.1.2 at 5.9 kB minified and gzipped for JavaScript, with a separate 6,092-byte gzipped stylesheet. The largest dependency is refractor at 56% of the bundle, and the package is not side-effect free, so tree-shaking is limited. If you do not need code highlighting, the @uiw/react-md-editor/nohighlight entry point drops rehype-prism-plus.

The preview prop accepts 'live', 'edit', or 'preview', and the component follows the controlled pattern with value and onChange. Sanitization is not applied by default. The official docs state: "Please note markdown needs to be sanitized if you do not completely trust your authors. Otherwise, your app is vulnerable to XSS." You add rehype-sanitize through previewOptions.

For Next.js App Router, wrap the editor in a Client Component and load it with next/dynamic and ssr: false. Since Next.js 15, ssr: false throws an error inside Server Components (per the Next.js 15 release), and that restriction carries through Next.js 16, where Turbopack is the default bundler.

// app/components/MarkdownEditor.jsx
'use client';
import dynamic from 'next/dynamic';
import rehypeSanitize from 'rehype-sanitize';

const MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false });

export function MarkdownEditor({ value, onChange }) {
  return (
    <MDEditor
      value={value}
      onChange={onChange}
      preview="live"
      previewOptions={{ rehypePlugins: [[rehypeSanitize]] }}
    />
  );
}

The README also documents installing next-remove-imports and importing @uiw/react-md-editor/markdown-editor.css and @uiw/react-markdown-preview/markdown.css explicitly. Even with that setup, issue #682 (open) reports code syntax highlighting failing under Turbopack. Strapi's guide to React Server Components with Strapi 5 covers where the client boundary should sit in this kind of setup.

Three limitations show up in the tracker, which holds 175 open issues:

  • CSS overrides. The TextArea stylesheet sets font-size, line-height, and font-family with !important, and the README confirms consumers need !important to override font size. Issue #663 asks for a reliable way to customize base styles and remains open.
  • Accessibility. Issue #686 reports the id prop lands on a parent <div> rather than the <textarea>, breaking label association; issue #452 reports that Preview mode cannot be exited via keyboard.
  • Cursor position. Five open issues track cursor jumps, the most recent being issue #700 from January 2026.

It fits comment boxes, blog post editors, and admin-panel fields where the default styling is acceptable and you control who authors content.

MDXEditor: Inline WYSIWYG for Markdown and JSX Components

MDXEditor is built on Meta's Lexical framework and edits Markdown inline with no separate preview pane. Version 4.2.4 was published September 9, 2026, depends on lexical ^0.48.0, and declares react >= 18 || >= 19 as peer dependencies. Weekly downloads were about 892,000 for September 5 to 11, 2026, and the repository has 87 open issues.

The bundle size figure many comparisons still quote is stale. Bundlephobia confirms 851.1 kB gzipped for 3.45.1, but 4.2.4 measures 564 kB gzipped (1.7 MB minified), a 33% reduction. Top contributors are @radix-ui/react-icons (13.7%), @codemirror/legacy-modes (8.2%), and @codemirror/view (7.0%). That is still roughly 95 times the JavaScript payload of @uiw/react-md-editor, and a dynamic import() of a module that large kicks off a substantial script evaluation task that can block input if the user interacts at the same moment. Loading the editor on demand behind a user action and a Suspense fallback can defer that cost beyond the initial render.

Features come in through plugin functions:

// components/MdxEditor.jsx
'use client';
import { MDXEditor, headingsPlugin, listsPlugin, linkPlugin, tablePlugin } from '@mdxeditor/editor';
import '@mdxeditor/editor/style.css';

export function Editor({ markdown, onChange }) {
  return (
    <MDXEditor
      markdown={markdown}
      onChange={onChange}
      plugins={[headingsPlugin(), listsPlugin(), linkPlugin(), tablePlugin()]}
    />
  );
}

Install with npm i @mdxeditor/editor@4.2.4. The getting-started guide recommends a 'use client' wrapper file imported through dynamic({ ssr: false }). Custom toolbars use the Gurx reactive state system through useCellValue, useCellValues, and usePublisher, documented in extending the editor. Image uploads go through an imageUploadHandler that receives a File and returns a Promise<string>.

Built-in features include a table editor, front-matter editing, code blocks with syntax highlighting, and a source/diff view. The JSX embedding story has caveats:

  • Discussion #629 on GenericJsxEditor failing to render ReactNode components inline is still open; 4.2.3 added protection against JSX kind mismatches crashing the editor but did not resolve it.
  • Issue #310 (pasting tables from Excel or Google Sheets yields plain text) and issue #784 (pasting tables into cells produces malformed Markdown) are both open.
  • Issue #544 reports the accessible name is hardcoded to aria-label="editable markdown" and cannot be changed.
  • Issue #699 documents Suspense breaking decorator nodes on React 18 only; React 19 is unaffected.

MDXEditor suits documentation sites and knowledge bases where editors need WYSIWYG behavior and the authoring team is trusted. If you are pairing it with a React 19 frontend, the React 19 blog tutorial with Strapi 5 covers the rendering side.

Milkdown: ProseMirror Framework With the Crepe Prebuilt UI

Milkdown describes itself as "a plugin-driven WYSIWYG markdown Editor, inspired by Typora, built on top of prosemirror and remark." All packages sit at 7.22.1, published August 12, 2026. For September 5 to 11, 2026, weekly downloads ran about 261,000 for @milkdown/core, 115,000 for @milkdown/react, and 213,000 for @milkdown/crepe.

Crepe is the piece that changed the calculus. It is "a rich-featured editor built on top of Milkdown" with Cursor, ListItem, LinkTooltip, ImageBlock, BlockEdit, Placeholder, Toolbar, CodeMirror, Table, and Latex enabled by default, plus three themes (Classic, Nord, Frame) in light and dark variants, per the Crepe API docs. The earlier complaint that Milkdown requires building every piece of UI by hand no longer holds when you use Crepe. Note that @milkdown/crepe depends on Vue 3, DOMPurify, and katex, so it pulls a second framework into a React app.

Bundlephobia returns no size data for @milkdown/core, @milkdown/react, or @milkdown/crepe, so no gzipped figure can be reported here. Milkdown resolved a tree-shaking issue in 7.12.1 by adding CrepeBuilder, which the maintainers say "allows for better tree-shaking and results in a smaller bundle size compared to using the full Crepe editor" (issue #1533).

The React recipe uses a provider, a hook, and a component:

// components/MilkdownEditor.tsx
import { Editor, rootCtx } from '@milkdown/kit/core';
import { commonmark } from '@milkdown/kit/preset/commonmark';
import { Milkdown, MilkdownProvider, useEditor } from '@milkdown/react';

const MilkdownEditor: React.FC = () => {
  useEditor((root) =>
    Editor.make()
      .config((ctx) => { ctx.set(rootCtx, root); })
      .use(commonmark)
  );
  return <Milkdown />;
};

export const MilkdownEditorWrapper: React.FC = () => (
  <MilkdownProvider>
    <MilkdownEditor />
  </MilkdownProvider>
);

Install with npm install @milkdown/react @milkdown/kit. Because ProseMirror needs a DOM, the maintainers' guidance in issue #389 is ssr: false with a default-export wrapper; the official FAQ does not provide dedicated Next.js App Router guidance.

The React ergonomics complaint remains unresolved. Discussion #1120, opened September 2023, states the integration is "a bit bare-bones and unconventional" because "props, event handlers, hook composition, component composition, controlled/uncontrolled components are not present." It has no marked answer. A September 2025 comment reports that passing a Crepe instance to useEditor "works visually but creates tons of uncaught errors," and an October 2025 comment says the request "is still very much desired." You handle change events through the listener plugin rather than an onChange prop.

Milkdown earns its learning curve when you need collaborative editing, math rendering, or deep document-model control.

react-markdown-editor-lite: Parser-Agnostic Split-Screen Editor

react-markdown-editor-lite is a split-screen editor where you supply your own parser (markdown-it, marked, or a custom function) through a renderHTML prop. Earlier versions of this comparison called it abandoned; the current record is more moderate. Version 1.4.2 was published January 21, 2026, the repository has 48 open issues, and weekly downloads were around 45,000 for September 5 to 11, 2026.

The README states it "Supports TypeScript" with bundled declarations. On SSR it is explicit: "If you are using a server-side render framework, like Next.js, Gatsby, please use client-side render," with the same dynamic(() => import('react-markdown-editor-lite'), { ssr: false }) pattern as the others.

// components/LiteEditor.jsx
import MdEditor from 'react-markdown-editor-lite';
import MarkdownIt from 'markdown-it';
import 'react-markdown-editor-lite/lib/index.css';

const mdParser = new MarkdownIt();

export function LiteEditor({ value, onChange }) {
  return (
    <MdEditor
      value={value}
      style={{ height: '500px' }}
      renderHTML={(text) => mdParser.render(text)}
      onChange={({ text }) => onChange(text)}
    />
  );
}

markdown-it's security guidance is "Don't enable HTML. Extend markup features with plugins." Leave html: false (the default) unless you also run the output through a sanitizer. A single release in the past year and 48 open issues put this package behind @uiw/react-md-editor on maintenance, but it is not frozen.

react-simplemde-editor: EasyMDE Wrapper Frozen Since 2022

react-simplemde-editor wraps EasyMDE. Version 5.2.0 was published October 1, 2022, and the last commit landed October 11, 2022, roughly four years without a release. It still recorded about 113,000 weekly downloads for September 5 to 11, 2026, and the repository lists seven open issues. It does ship TypeScript declarations in typings/SimpleMdeReact.d.ts.

The dependency pin is the concrete problem. The package requires easymde >= 2.0.0 < 3.0.0. Upstream EasyMDE is actively maintained, released 2.21.0 on May 3, 2026, recorded about 203,000 weekly downloads for September 5 to 11, 2026, and has published 3.0.0-beta.1 to npm. Once EasyMDE version 3 lands, the wrapper cannot consume it without a new release from a repository that has been silent for four years. The version 5 breaking changes also note "SSR safe nets removed, please make sure to import it dynamically."

If you like EasyMDE's toolbar and autosave, the safer path is a thin wrapper of your own around the easymde package, which keeps you on the maintained line.

React Markdown Editors Side by Side

EditorVersion (published)Gzipped sizeEditing modelReact integrationMaintenance signal
@uiw/react-md-editor4.1.2 (Aug 2026)5.9 kB JS + 6 kB CSSTextarea + live previewControlled value/onChange175 open issues, active releases
MDXEditor4.2.4 (Sep 2026)564 kBInline WYSIWYG (Lexical)markdown/onChange props, plugin array87 open issues, active releases
Milkdown7.22.1 (Aug 2026)Not reported by BundlephobiaInline WYSIWYG (ProseMirror)Provider + useEditor, no native propsActive; #1120 open
react-markdown-editor-lite1.4.2 (Jan 2026)Not measured in this reportSplit-screen, bring your own parservalue/onChange/renderHTML48 open issues, one release in past year
react-simplemde-editor5.2.0 (Oct 2022)Not measured in this reportTextarea + preview (EasyMDE)value/onChange/optionsNo release since 2022; pins EasyMDE < 3

Rendering User-Authored Markdown Without XSS

Editor previews and frontend rendering are both security surfaces when user-authored markup reaches an unsafe HTML sink. OWASP recommends HTML sanitization (specifically DOMPurify) rather than output encoding when users author markup, and warns that "If you sanitize content and then modify it afterwards, you can easily void your security efforts."

For React, react-markdown is "secure by default" because it avoids dangerouslySetInnerHTML and escapes raw HTML. If you enable raw HTML through rehype-raw, the rehype-sanitize README says to add sanitization "after what plugins do," meaning rehypeSanitize runs after rehypeRaw in the pipeline. GFM's tagfilter extension only neutralizes nine specific tags such as <script> and <iframe>; it is not a sanitizer.

Store raw Markdown in the database rather than pre-rendered HTML, as OWASP ASVS guidance recommends. That avoids double-encoding, lets you change renderers later, and keeps sanitization at the rendering boundary. If your content also targets MDX, note that CVE-2026-0969 allowed arbitrary code execution in next-mdx-remote 4.3.0 to 5.0.0 through insufficient MDX sanitization; 6.0.0 fixed it and now disables JavaScript expressions by default.

Using a React Markdown Editor as a Strapi 5 Custom Field

Strapi 5's Admin Panel already includes a built-in Markdown editor for richtext fields, with a Preview/Markdown toggle and side-by-side mode. The separate Rich Text (Blocks) field stores JSON instead of Markdown, and Strapi recommends the Blocks React Renderer for it in the Content-Type Builder docs; the Blocks editor walkthrough with Next.js covers that path.

To swap the Markdown editor for one of the libraries above, you have three routes, all documented under Custom Fields:

  • Community Marketplace plugin. The react-md-editor plugin wraps @uiw/react-md-editor as a custom field with a configurable toolbar. It is a community package, not Strapi-maintained, so check its Strapi 5 compatibility before depending on it.
  • Your own plugin. Scaffold with the Plugin SDK, register the field server-side and admin-side, and supply an Input component. Free, and distributable to the Marketplace.
  • App-specific registration in src/index and src/admin/app. Free and simplest, but not publishable.

Custom fields are free and enabled by default. Constraints to plan around: a custom field must map to an existing Strapi data type (no relations, media, components, or dynamic zones), and the Content-Type Builder only runs in the development environment because schemas live in files (per the Strapi FAQ). For Markdown, use text (unlimited length) or richtext; string caps at 255 characters, per the models documentation.

Scaffold the plugin with the Plugin SDK, which Strapi 5 introduced alongside the Document Service API (covered in the developer experience post):

npx @strapi/sdk-plugin init markdown-field

Register the field on the server in the plugin's register lifecycle:

// src/plugins/markdown-field/server/register.js
module.exports = ({ strapi }) => {
  strapi.customFields.register({
    name: 'markdown',
    plugin: 'markdown-field',
    type: 'text',
  });
};

Then register it on the admin side. Strapi 5 requires .jsx or .tsx for admin files, and app.customFields.register() must run in register(app), not bootstrap(app), per the Admin Panel API:

// src/plugins/markdown-field/admin/src/index.jsx
export default {
  register(app) {
    app.customFields.register({
      name: 'markdown',
      pluginId: 'markdown-field',
      type: 'text',
      intlLabel: { id: 'markdown-field.label', defaultMessage: 'Markdown' },
      intlDescription: { id: 'markdown-field.description', defaultMessage: 'Markdown with live preview' },
      components: {
        Input: async () => import('./components/Input').then((module) => ({ default: module.Input })),
      },
    });
  },
};

The Input component receives name, value, onChange, attribute, required, error, and disabled. Strapi's contract for onChange is ({ target: { name, type, value } }), where type is the underlying Strapi type. Bind the editor to value; uncontrolled inputs will not submit on save. Wrap the component in React.forwardRef so auto-focus works:

// src/plugins/markdown-field/admin/src/components/Input.jsx
import React from 'react';
import MDEditor from '@uiw/react-md-editor';

export const Input = React.forwardRef(({ name, value, onChange, attribute }, ref) => (
  <div ref={ref}>
    <MDEditor
      value={value ?? ''}
      onChange={(next) =>
        onChange({ target: { name, type: attribute.type, value: next ?? '' } })
      }
    />
  </div>
));

Enable the local plugin in config/plugins.js with enabled: true and resolve: './src/plugins/markdown-field', per the plugins configuration docs. After a restart, the field appears in the Content-Type Builder under Custom. The tutorials on building a plugin for Strapi 5 and building a custom field with an admin widget walk through the same lifecycle with more scaffolding detail.

Two points of ownership: Strapi validates the Markdown field through its content model, persists the string to the configured database, and exposes it through the auto-generated REST API, subject to roles and permissions. To expose it through GraphQL, install and configure Strapi's GraphQL plugin. The REST and GraphQL overview compares the two; rendering and sanitizing the Markdown belong to your frontend, following the pipeline in the previous section. And if you use Strapi AI to generate content types from a description or a Figma link, that assistant is a Growth plan feature (Strapi 5.30+), not part of the free tier; the custom field itself works on any plan.

Which React Markdown Editor Fits Your Stack

For a Strapi admin field, a comment box, or any editor where you accept default styling, @uiw/react-md-editor is the practical default: 5.9 kB of JavaScript, bundled types, and a release from August 2026. Budget time for !important overrides and audit the open accessibility issues if you have compliance requirements.

Pick MDXEditor when authors expect inline WYSIWYG and you want tables, front matter, and code blocks without assembling plugins yourself. Load its 564 kB on demand and skip the JSX-embedding features until discussion #629 closes.

Reach for Milkdown with Crepe when collaborative editing, math, or deep document-model control matters more than a conventional React API. Treat react-markdown-editor-lite as a maintained-but-slow option, and replace react-simplemde-editor with a direct EasyMDE wrapper or one of the first two editors.

Whichever you choose, the field it produces can be stored as Markdown text that your frontend has to parse and sanitize. If you are building the Strapi side, the guide to extending the admin and backend with a Strapi plugin picks up where the custom field registration above leaves off; for the rendering side in Next.js, the documentation site tutorial with Strapi 5 and Next.js covers fetching and rendering Markdown content end to end.

Paul BratslavskyDeveloper Advocate

Related Posts

Convert RTF to Markdown
Ecosystem·14 min read

Stuck with 3,700 RTF Files? Here's How to Convert Them to Markdown Fast

Convert thousands of RTF files to Markdown using Pandoc, Node.js, or Python. Automate batch processing, cleanup, and CI/CD integration for production.

·November 13, 2025
Editing Features
Ecosystem·11 min read

5 Visual Editing Features That Make Your Headless CMS Accessible to Content Teams

Restore visual editing to headless CMS with Dynamic Zones, Live Preview, and block editors. Complete guide to visual editing for content teams.

·December 3, 2025
How to change the WYSIWYG in Strapi
Tutorials·5 min read

How to change the WYSIWYG in Strapi

This tutorial explains how to easily do an Admin Customization in Strapi. This example shows how to replace the markdown rich text editor by a WYSIWYG editor.

·October 23, 2019