pd-editor-react
v2.1.0
Published
React Markdown editor for technical content and AI writing workflows
Maintainers
Readme
pd-editor-react
A React Markdown editor for technical content and AI writing workflows, with CodeMirror 6, live Mermaid and math preview, frontmatter, linting, image upload, and TypeScript-first APIs.
Designed for product docs, CMS editors, AI writing tools, admin dashboards, developer portals, note-taking apps, and any React interface that needs a serious Markdown editing experience without building the editor stack from scratch.
Try the interactive demo: laochen1994.github.io/pd-markdown-editor.
Highlights
- ⚛️ React-first adapter - controlled/uncontrolled component plus
useMarkdownEditorfor custom shells. - 🚀 Powered by CodeMirror 6 - fast editing, history, search, folding, completions, Markdown syntax support.
- 👀 Three preview modes -
edit,split, andpreview. - 🎨 Beautiful Markdown preview - rendered through
pd-markdown+pd-markdown-ui. - 🧠 Markdown-aware typing - continue lists/tasks/quotes on
Enter, indent/outdent withTab. - ⌨️ Rich shortcuts - formatting, headings, links, lists, quote, save, and typing flow.
- 🧰 Toolbar included - default toolbar, custom toolbar, plugin toolbar items.
- 🖼️ Image upload plugin - paste and drag image upload through
pd-editor-core. - 🧭 TOC plugin - live heading navigation with stable parser-generated ids.
- 🧩 Composable extension model - custom CodeMirror extensions, custom preview renderers, runtime plugins.
- 🧱 Headless entry - opt out of automatic styles and import CSS manually.
- 🌓 Light/dark themes - consistent editor and preview appearance.
Install
pnpm add pd-editor-react
# npm install pd-editor-react
# yarn add pd-editor-reactreact and react-dom are peer dependencies. The adapter installs pd-editor-core and includes the default preview styles.
Quick Start
import { useState } from "react";
import { MarkdownEditor } from "pd-editor-react";
export const App = () => {
const [value, setValue] = useState("# Hello pd-editor-react");
return (
<MarkdownEditor
value={value}
onChange={setValue}
theme="light"
preview="split"
height={640}
placeholder="Write Markdown..."
/>
);
};The default entry imports the editor preview styles for you:
import { MarkdownEditor } from "pd-editor-react";For full CSS control, use the headless entry:
import { MarkdownEditor } from "pd-editor-react/headless";
import "pd-editor-react/styles.css";Feature Matrix
| Area | Support |
| --- | --- |
| Component mode | Controlled value, uncontrolled defaultValue |
| Preview | edit, split, preview |
| Styling | Styled root entry, headless entry, explicit styles.css |
| Markdown UI | Headings, paragraphs, lists, task lists, code, table, blockquote, heading ids |
| Toolbar | Built-in toolbar, custom toolbar items, plugin toolbar items |
| Plugins | imageUploadPlugin, tocPlugin, custom plugins |
| Commands | Formatting, headings, lists, quote, code, link, image, table, horizontal rule |
| Runtime controls | theme, readOnly, controlled value sync |
| Advanced | CodeMirror extensions, custom preview component map |
Fenced code previews render lightweight semantic <pre><code> markup by default. Override renderComponentMap.code when your application needs a specific syntax highlighter.
Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| value | string | - | Controlled Markdown value |
| defaultValue | string | "" | Initial value for uncontrolled usage |
| onChange | (value: string) => void | - | Called after content changes |
| onSave | (value: string) => void | - | Called by Ctrl/Cmd+S |
| theme | "light" \| "dark" | "light" | Editor and preview theme |
| placeholder | string | - | CodeMirror placeholder |
| readOnly | boolean | false | Prevent user and command edits |
| height | string \| number | "500px" | Editor container height |
| preview | "edit" \| "preview" \| "split" | "edit" | View mode |
| toolbar | boolean \| ToolbarItem[] | true | Built-in toolbar, hidden toolbar, or custom items |
| plugins | EditorPlugin[] | [] | Core plugins |
| extensions | Extension[] | [] | CodeMirror 6 extensions |
| codeLanguages | MarkdownCodeLanguages | - | Optional fenced code language resolver for the editor |
| renderComponentMap | Partial<ComponentMap> | - | Override Markdown preview components |
| className | string | "" | Outer container class |
| style | React.CSSProperties | {} | Outer container style |
Preview Modes
<MarkdownEditor preview="edit" />
<MarkdownEditor preview="split" />
<MarkdownEditor preview="preview" value={markdown} />split mode keeps the editor and preview side-by-side. preview mode renders Markdown without mounting the editor.
Keyboard Shortcuts
| Shortcut | Action |
| --- | --- |
| Ctrl/Cmd+B | Bold |
| Ctrl/Cmd+I | Italic |
| Ctrl/Cmd+K | Link |
| Ctrl/Cmd+Shift+X | Strikethrough |
| Ctrl/Cmd+Alt+1 | Heading 1 |
| Ctrl/Cmd+Alt+2 | Heading 2 |
| Ctrl/Cmd+Alt+3 | Heading 3 |
| Ctrl/Cmd+Shift+7 | Ordered list |
| Ctrl/Cmd+Shift+8 | Bullet list |
| Ctrl/Cmd+Shift+9 | Quote |
| Ctrl/Cmd+S | onSave |
| Enter | Continue list/task/ordered/quote block |
| Tab | Indent Markdown block |
| Shift+Tab | Outdent Markdown block |
Toolbar Commands
The default toolbar includes:
bold, italic, strikethrough, heading1, heading2, heading3, unorderedList, orderedList, taskList, link, image, quote, code, codeBlock, table, horizontalRule.
Disable it:
<MarkdownEditor toolbar={false} />Custom toolbar:
import type { ToolbarItem } from "pd-editor-core";
const toolbar: ToolbarItem[] = [
{ command: "bold", label: "Bold", icon: "<strong>B</strong>", shortcut: "Ctrl+B" },
{ command: "heading2", label: "H2", icon: "<span>H2</span>" },
{ command: "divider", label: "", icon: "", divider: true },
{ command: "link", label: "Link", icon: "<span>🔗</span>", shortcut: "Ctrl+K" },
];
<MarkdownEditor toolbar={toolbar} />;Custom toolbar icon values are trusted HTML from your application code. Do not pass untrusted user content into icon.
Plugins
Image Upload
import { useMemo, useState } from "react";
import { MarkdownEditor } from "pd-editor-react";
import { imageUploadPlugin } from "pd-editor-core";
export const EditorWithUpload = () => {
const [value, setValue] = useState("");
const plugins = useMemo(() => [
imageUploadPlugin({
maxSize: 5 * 1024 * 1024,
handler: async (file) => {
const form = new FormData();
form.append("file", file);
const response = await fetch("/api/upload", {
method: "POST",
body: form,
});
const data = await response.json() as { url: string };
return data.url;
},
}),
], []);
return (
<MarkdownEditor
value={value}
onChange={setValue}
plugins={plugins}
preview="split"
/>
);
};Table Of Contents
import { useEffect, useRef, useState } from "react";
import { MarkdownEditor } from "pd-editor-react";
import { tocPlugin, type EditorPlugin } from "pd-editor-core";
export const EditorWithToc = () => {
const tocRef = useRef<HTMLDivElement>(null);
const [plugins, setPlugins] = useState<EditorPlugin[]>([]);
useEffect(() => {
if (tocRef.current) {
setPlugins([tocPlugin({ container: tocRef.current, maxLevel: 3 })]);
}
}, []);
return (
<div style={{ display: "grid", gridTemplateColumns: "1fr 220px", gap: 16 }}>
<MarkdownEditor defaultValue="# Intro" preview="split" height={640} plugins={plugins} />
<div ref={tocRef} />
</div>
);
};Custom Preview Rendering
React preview uses pd-markdown/web mdast component keys. Override only what you need:
import type { ComponentMap } from "pd-markdown/web";
const renderComponentMap: Partial<ComponentMap> = {
heading: ({ node, children }) => {
const Tag = `h${node.depth}` as "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
const id = typeof node.data?.id === "string" ? node.data.id : undefined;
return <Tag id={id} data-heading>{children}</Tag>;
},
blockquote: ({ children }) => (
<blockquote className="my-callout">{children}</blockquote>
),
};
<MarkdownEditor renderComponentMap={renderComponentMap} preview="split" />;Common keys include heading, paragraph, list, listItem, table, tableRow, tableCell, code, inlineCode, and blockquote.
Raw HTML Markdown nodes are not rendered by the default preview. If your application needs embedded HTML, sanitize it before rendering it through a custom preview pipeline.
Hook API
Use useMarkdownEditor when you want to build a completely custom shell around the core editor:
import { useMarkdownEditor } from "pd-editor-react";
export const CustomShell = () => {
const { containerRef, getValue, executeCommand } = useMarkdownEditor({
initialValue: "# Custom shell",
onChange: (value) => console.log(value),
});
const logValue = () => {
console.log(getValue());
};
return (
<>
<button type="button" onClick={() => executeCommand("bold")}>Bold</button>
<button type="button" onClick={logValue}>Log</button>
<div ref={containerRef} style={{ height: 500 }} />
</>
);
};Initialization options are read when the editor mounts. theme and readOnly are synchronized after mount; structural options such as initialValue, placeholder, toolbar, plugins, extensions, and codeLanguages are not rebuilt automatically. Recreate the hook owner when those structural options need to change.
SSR / Next.js
The editor requires a browser DOM. In SSR frameworks, render it from a client boundary.
"use client";
import { MarkdownEditor } from "pd-editor-react";
export const ClientMarkdownEditor = () => (
<MarkdownEditor defaultValue="# Client only" preview="split" />
);FAQ
Do I need to import CSS?
If you import pd-editor-react, base preview styles are included automatically. If you import pd-editor-react/headless, import pd-editor-react/styles.css yourself.
Can I use it as a controlled component?
Yes. Pass value and onChange. External value updates are synchronized into the editor without calling onChange again. For one-time initial content, use defaultValue.
Can I hide preview or toolbar?
Yes. Use preview="edit" and toolbar={false}.
Can I customize Markdown preview components?
Yes. Use renderComponentMap with pd-markdown/web mdast keys.
Can I access the underlying editor?
Use useMarkdownEditor for direct access to the core editor ref. The component API is intentionally higher-level.
Is this only for docs sites?
No. It works well for CMS fields, support dashboards, AI writing surfaces, changelog editors, internal tools, product docs, and note apps.
Related Packages
pd-editor-core- Framework-agnostic CodeMirror editor core.pd-editor-vue- Vue 3 adapter.pd-markdown- Markdown parser/renderer.pd-markdown-ui- Styled Markdown preview primitives.
License
MIT
