A split-screen monitor showing raw markdown on the left and rendered preview on the right, with a gray cat watching from the windowsill.

React MD Editor: The Textarea That Thinks It's an Editor

The Gray Cat
The Gray Cat
0 views

If you have ever needed a markdown editor in a React app, you have probably felt the gravitational pull toward something heavy. CodeMirror, Monaco, a Lexical-based rich-text monster with a plugin for every occasion. They are powerful, but they are also a lot of editor for what is often a humble job: let a user type some markdown, show them what it will look like, save the string. React MD Editor (@uiw/react-md-editor) takes the opposite bet. It is built on a plain <textarea> with a live preview pane bolted alongside it, and that single design choice keeps it small, fast to drop in, and refreshingly free of configuration ceremony.

The result is a component you can add in two lines and start using immediately, with a split-pane "type on the left, see it rendered on the right" experience that feels right at home for anyone who has used a GitHub comment box. It is TypeScript-first, MIT-licensed, and pulls down well over 700,000 downloads a week, so you are in good company. Let us look at what it can do.

Why a Textarea Instead of a Real Editor

The core idea behind React MD Editor is that markdown is just text, and a <textarea> is a perfectly good place to type text. By skipping a dedicated code-editor engine, the library avoids the bundle weight and dependency sprawl that usually comes with markdown tooling. The preview rendering is delegated to its sibling library @uiw/react-markdown-preview, which wraps react-markdown and the unified/rehype/remark plugin chain underneath, so you still get proper GitHub-flavored markdown with syntax-highlighted code blocks.

What you get out of the box is genuinely useful:

  • Three view modeslive (split edit and preview), edit only, and preview only.
  • GitHub-flavored markdown with auto list continuation, so pressing Enter inside a list keeps it going.
  • Dark and light mode with automatic system-preference detection.
  • Syntax highlighting in the preview for fenced code blocks.
  • A fully customizable toolbar built from importable, reorderable commands.
  • Opt-in extras like KaTeX math, Mermaid diagrams, and HTML sanitization.

What you do not get is a true WYSIWYG caret experience. There is no inline formatting that floats over your selection like a word processor. It is, at the end of the day, a very well-dressed textarea. For a huge number of apps, that is exactly the right trade.

Getting It Into Your Project

Installation is a single package with a small dependency footprint.

npm install @uiw/react-md-editor

Or with yarn:

yarn add @uiw/react-md-editor

The only peer dependencies are React and React DOM at version 16.8 or newer, since the component relies on hooks. Most bundlers will pick up the component's CSS automatically, but in some setups (notably Next.js and certain Vite configurations) you will want to import the stylesheets explicitly:

import "@uiw/react-md-editor/markdown-editor.css";
import "@uiw/react-markdown-preview/markdown.css";

Your First Editor in Two Props

The whole point of this library is how little ceremony it takes. Wire up a piece of state, pass value and onChange, and you have a working editor.

import React from "react";
import MDEditor from "@uiw/react-md-editor";

export default function App() {
  const [value, setValue] = React.useState<string>("**Hello world!!!**");

  return (
    <div className="container">
      <MDEditor value={value} onChange={(val) => setValue(val ?? "")} />
      <MDEditor.Markdown source={value} style={{ whiteSpace: "pre-wrap" }} />
    </div>
  );
}

Notice the second component, MDEditor.Markdown. That is the standalone renderer, the same engine the preview pane uses, exposed so you can render markdown anywhere in your app without the editor chrome. It is perfect for displaying saved content on a read-only page. The onChange handler receives the new value (which can be undefined, hence the fallback), and optionally the change event and editor state if you need them.

Choosing What the User Sees

By default the editor shows the split live view. You can switch panes with the preview prop, and tweak the dimensions while you are at it.

<MDEditor
  value={value}
  onChange={(val) => setValue(val ?? "")}
  preview="edit"
  height={300}
  visibleDragbar={true}
/>

Setting preview to "edit" gives you a focused writing surface with no preview, "preview" renders the result only, and "live" keeps both side by side. The height prop sets the editor's starting height, and visibleDragbar shows a handle at the bottom so users can resize it themselves. If you let them drag, minHeight and maxHeight keep the resizing within sane bounds.

Tuning the Textarea Itself

Because the editor wraps a native textarea, you can pass standard HTML attributes straight through with textareaProps. This is the natural home for a placeholder, an autocomplete setting, or a maximum length.

<MDEditor
  value={value}
  onChange={(val) => setValue(val ?? "")}
  textareaProps={{
    placeholder: "Start writing your masterpiece in markdown...",
    maxLength: 5000,
  }}
  tabSize={4}
/>

The tabSize prop controls how many spaces a Tab key press inserts, which is handy when your content includes indented code. If you would rather the Tab key move focus the way it normally does in a form, set defaultTabEnable to restore native behavior.

Reshaping the Toolbar

The toolbar is where React MD Editor stops being just a textarea and starts feeling configurable. Every button is a command you can import, reorder, drop, or replace. The library exports a commands object full of presets, and two props let you control the two halves of the toolbar: commands for the main left side and extraCommands for the right side.

import MDEditor, { commands } from "@uiw/react-md-editor";

<MDEditor
  value={value}
  onChange={(val) => setValue(val ?? "")}
  commands={[
    commands.bold,
    commands.italic,
    commands.strikethrough,
    commands.divider,
    commands.link,
    commands.quote,
    commands.code,
  ]}
  extraCommands={[commands.codeEdit, commands.codeLive, commands.codePreview]}
/>

This trims the toolbar down to a curated set and keeps the view-mode toggles tucked away on the right. If you want a minimal experience with no toolbar at all, the hideToolbar prop does exactly that.

Writing a Custom Command

When the presets are not enough, you can implement your own command. A command is an object describing its name, an optional keyboard shortcut, the button's props, an icon, and an execute function that receives the current editor state and an API for manipulating the text.

import MDEditor, { commands, ICommand, TextState, TextAreaTextApi } from "@uiw/react-md-editor";

const insertTitle3: ICommand = {
  name: "title3",
  keyCommand: "title3",
  buttonProps: { "aria-label": "Insert H3" },
  icon: <span style={{ fontWeight: 700 }}>H3</span>,
  execute: (state: TextState, api: TextAreaTextApi) => {
    const prefix = "### ";
    const text = state.selectedText ? `${prefix}${state.selectedText}\n` : prefix;
    api.replaceSelection(text);
  },
};

<MDEditor
  value={value}
  onChange={(val) => setValue(val ?? "")}
  commands={[commands.bold, commands.italic, insertTitle3]}
/>;

The execute function is where the logic lives. The state object tells you what the user has selected, and the api lets you replace that selection, set the cursor, and so on. This same mechanism powers everything from "insert an image" to "embed a custom widget syntax," so any markdown shortcut your app needs can become a toolbar button.

Theming, Plugins, and the Hard Cases

The preview pane accepts a previewOptions prop that is passed straight through to the underlying react-markdown renderer. This is your gateway to the entire unified plugin ecosystem, which is how the library handles the more demanding features without bloating the default bundle.

Sanitizing Untrusted Markdown

By default the preview will happily render raw HTML embedded in markdown. That is fine for trusted content, but if you are rendering anything a user typed, you should add rehype-sanitize to strip out dangerous markup and protect against XSS.

import MDEditor from "@uiw/react-md-editor";
import rehypeSanitize from "rehype-sanitize";

<MDEditor
  value={value}
  onChange={(val) => setValue(val ?? "")}
  previewOptions={{
    rehypePlugins: [[rehypeSanitize]],
  }}
/>;

This is one of those settings that is easy to forget and important to remember. Sanitization is opt-in precisely because it is not always wanted, but for any public-facing editor it should be on.

Forcing Dark Mode

The editor detects the system color scheme automatically, but you can override it by setting an attribute on the document element. This pairs naturally with a theme toggle in your app.

function setTheme(mode: "light" | "dark") {
  document.documentElement.setAttribute("data-color-mode", mode);
}

The theme variables are exposed through the .wmde-markdown-var CSS selector, so if you want to match the editor to your brand rather than the defaults, you can override those custom properties in your own stylesheet.

Surviving Next.js and Server Rendering

Because the editor depends on the DOM and a real textarea, it cannot render on the server. In a Next.js app, import it dynamically with SSR disabled so it only mounts in the browser.

import dynamic from "next/dynamic";
import "@uiw/react-md-editor/markdown-editor.css";
import "@uiw/react-markdown-preview/markdown.css";

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

This is the single most common gotcha people hit, and the fix is exactly this one pattern. Once the import is dynamic and SSR is off, everything else behaves the same as in a plain client app.

Trimming the Bundle Further

Syntax highlighting in the preview is the biggest contributor to the package's size, so the library offers alternate entry points to control that cost. If your content rarely contains code, or you want a smaller payload, you can pick a leaner build.

// Full language highlighting (default, largest)
import MDEditor from "@uiw/react-md-editor";

// A curated subset of common languages
import MDEditor from "@uiw/react-md-editor/common";

// No code highlighting at all (smallest)
import MDEditor from "@uiw/react-md-editor/nohighlight";

The /common entry point arrived in version 4.1.0 and hits a nice middle ground for most apps, covering the languages people actually paste while skipping the long tail. The /nohighlight build is ideal when your markdown is prose-heavy and you care about every kilobyte.

When This Is the Right Tool

React MD Editor occupies a sweet spot. It is lighter than a CodeMirror or Monaco setup, simpler than a full WYSIWYG editor like MDXEditor, and far more capable than rolling your own textarea-plus-react-markdown combination by hand. If you need a comment box, a documentation field, a blog post composer, or any place where users write markdown and want to see it rendered, this drops in cleanly and stays out of your way.

It will not give you a Notion-like rich-text experience, and the fancier features such as math and diagrams require wiring up plugins yourself rather than flipping a switch. But for the very common case of "let people write markdown and preview it," the textarea-based approach is not a limitation. It is the whole point, and it is why this little component shows up in three quarters of a million projects a week. Sometimes the unglamorous answer is the correct one.