From zbeyens. The source code is available on GitHub.

Plate
PlatePliteEditorsTemplates
GitHub16kGitHub
DiscordDiscord
  • Feature Kits
  • Plugin
    • Plugin Methods
    • Plugin Shortcuts
    • Plugin Context
    • Plugin Components
    • Plugin Rules
    • Editing Behavior
    • Plugin Input Rules
  • Editor
    • Editor Methods
    • Controlled Value
  • Performance
  • Static Rendering
  • HTML
  • Markdown
  • Form
  • TypeScript
  • Debugging
  • Unit Testing
  • Browser
  • Troubleshooting

Markdown

PreviousNext

Convert Plate content to Markdown and vice-versa.

@platejs/markdown converts Markdown to Plate documents and Plate documents back to Markdown.

Loading…
HTMLForm

On This Page

FeaturesWhy Use Plate Markdown?Kit UsageInstallationAdd KitManual UsageInstallationAdd PluginConfigure PluginMarkdown to Plate (Deserialization)Plate to Markdown (Serialization)Round-Trip Serialization with Custom Elements (MDX)API ReferenceMarkdownPluginStateMarkdownPlugineditor.api.markdown.deserializeeditor.api.markdown.deserializeInlineeditor.api.markdown.serializeparseMarkdownBlocksExamplesUsing a Remark Plugin (GFM)Customizing Rendering (Syntax Highlighting)Using Remark Plugins for Math (remark-math)Using Mentions (remarkMention)Using ColumnsRemark PluginsSyntax SupportArchitecture OverviewMigrating from react-markdownAppendix A: HTML in MarkdownAppendix B: Conversion OwnershipAppendix C: Components for RenderingAppendix D: PlateMarkdown Component (Read-Only Display)Security ConsiderationsRelated Links
Build your editor
Production-ready AI template and reusable components.
Get all-access
Loading…

Features

  • Markdown to Plate JSON: Convert Markdown strings to Plate's editable format (deserialize).
  • Plate JSON to Markdown: Convert Plate content back to Markdown strings (serialize).
  • Safe by Default: Handles Markdown conversion without dangerouslySetInnerHTML.
  • Feature-Owned Codecs: Installed feature plugins contribute their own Markdown conversions, including MDX nodes.
  • Extensible: Add remark plugins through remarkPlugins.
  • Compliant: Supports CommonMark, with GFM (GitHub Flavored Markdown) available via remark-gfm.
  • Round-Trip Serialization: Preserves custom elements through MDX syntax during conversion cycles.
Report an issue

Why Use Plate Markdown?

While libraries like react-markdown render Markdown to React elements, @platejs/markdown offers deeper integration with the Plate ecosystem:

  • Rich Text Editing: Enables advanced editing features by converting Markdown to Plate's structured format.
  • WYSIWYG Experience: Edit content in a rich text view and serialize it back to Markdown.
  • Custom Elements & Data: Handles complex custom Plate elements (mentions, embeds) by converting them to/from MDX.
  • Extensibility: Leverages Plate's plugin system and the unified/remark ecosystem for powerful customization.

If you only need to display Markdown as HTML without editing or custom elements, react-markdown might be sufficient. For a rich text editor with Markdown import/export and custom content, @platejs/markdown is the integrated solution.

Kit Usage

Installation

The fastest way to add Markdown functionality is with MarkdownKit. It combines the configured MarkdownPlugin with the live Footnote plugins and essential remark plugins for Plate UI.

import { MarkdownPlugin, remarkMdx, remarkMention } from '@platejs/markdown';
import { PLUGINS } from 'platejs';
import remarkEmoji from 'remark-emoji';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
 
export const MarkdownKit = [
  MarkdownPlugin.configure(({ editor }) => {
    const comment = editor.plugin(PLUGINS.comment);
    const suggestion = editor.plugin(PLUGINS.suggestion);
    const plainMarks: string[] = [];
 
    if (suggestion.installed) {
      plainMarks.push(suggestion.schema.key);
    }
    if (comment.installed) {
      plainMarks.push(comment.schema.key);
    }
 
    return {
      initialState: {
        plainMarks,
        remarkPlugins: [
          remarkMath,
          remarkGfm,
          remarkEmoji,
          remarkMdx,
          remarkMention,
        ],
      },
    };
  }),
];
import { MarkdownPlugin, remarkMdx, remarkMention } from '@platejs/markdown';
import { PLUGINS } from 'platejs';
import remarkEmoji from 'remark-emoji';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
 
export const MarkdownKit = [
  MarkdownPlugin.configure(({ editor }) => {
    const comment = editor.plugin(PLUGINS.comment);
    const suggestion = editor.plugin(PLUGINS.suggestion);
    const plainMarks: string[] =





















Add Kit

import { createPlateEditor } from 'platejs/react';
import { MarkdownKit } from '@/components/editor/markdown';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    ...MarkdownKit,
  ],
});
import { createPlateEditor } from 'platejs/react';
import { MarkdownKit } from '@/components/editor/markdown';
 
const editor = createPlateEditor




Manual Usage

Installation

pnpm add platejs @platejs/markdown
pnpm add platejs @platejs/markdown

Add Plugin

import { MarkdownPlugin } from '@platejs/markdown';
import { createPlateEditor } from 'platejs/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    MarkdownPlugin,
  ],
});
import { MarkdownPlugin } from '@platejs/markdown';
import { createPlateEditor } from 'platejs/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    MarkdownPlugin,
  ],
});

Configure Plugin

Configure MarkdownPlugin.initialState for shared syntax plugins, filtering, and stringification. Installed feature plugins contribute their Markdown node codecs automatically. The same state drives the Markdown API and the plugin's text/markdown and text/plain document codecs.

lib/plate-editor.ts
import { createPlateEditor } from 'platejs/react';
import {
  MarkdownPlugin,
  remarkMention,
  remarkMdx,
} from '@platejs/markdown';
import remarkEmoji from 'remark-emoji';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
 
const editor = createPlateEditor({
  plugins: [
    // ...other Plate plugins
    MarkdownPlugin.configure({
      initialState: {
        remarkPlugins: [
          remarkMath,
          remarkGfm,
          remarkEmoji,






MarkdownPlugin publishes conversion through editor.api.markdown. Plugin state remains under editor.plugin(MarkdownPlugin). Add or remove a feature plugin to add or remove its Markdown conversion.

Primary content only

Markdown is a projection of document.children. It does not preserve named roots or open slice edges. Clipboard transport uses application/x-plite-fragment when exact roots, openStart, and openEnd must survive.

Markdown to Plate (Deserialization)

Use editor.api.markdown.deserialize to convert a Markdown string into an EditorDocumentValue. Media captions are block children of their media element.

components/my-editor.tsx
import { createPlateEditor } from 'platejs/react';
import { MarkdownPlugin } from '@platejs/markdown';
import { ItalicPlugin } from '@platejs/basic-nodes/react';
import { BasicBlocksKit } from '@/components/editor/basic-blocks';
// ... import other necessary Plate plugins for rendering elements
 
const markdownString = '# Hello, *Plate*!';
 
const editor = createPlateEditor({
  plugins: [
    // MarkdownPlugin must be included
    MarkdownPlugin,
    ...BasicBlocksKit,
    ItalicPlugin,
  ],
  // Use deserialize in the value factory for initial content
  initialValue: ({ editor }) 

Plugin Requirements

Ensure all Plate plugins required to render the deserialized Markdown (e.g., HeadingPlugin for #, TablePlugin for tables) are included in your editor's plugins array.

Plate to Markdown (Serialization)

Use editor.api.markdown.serialize to convert the current editor document or a specific EditorDocumentValue into a Markdown string.

Serialization reads the document's primary children. Named roots are outside Markdown's representation.

Serializing Current Editor Content:

// Assuming `editor` is your Plate editor instance with content
const markdownOutput = editor.api.markdown.serialize();
console.info(markdownOutput);
// Assuming `editor` is your Plate editor instance with content
const markdownOutput = editor.api.markdown.serialize();
console.info(markdownOutput);

Serializing a Specific Document:

const specificNodes = [
  {
    children: [{ text: 'Serialize just this paragraph.' }],
    type: 'paragraph',
  },
  {
    children: [{ text: 'And this heading.' }],
    type: 'heading', level: 1,
  },
];
 
// Assuming `editor` is your Plate editor instance
const partialMarkdownOutput = editor.api.markdown.serialize({
  value: { children: specificNodes },
});
console.info(partialMarkdownOutput);

Markdown image alt text maps to one paragraph child of the image. Rich MDX media children map directly to the media element's block children.

Round-Trip Serialization with Custom Elements (MDX)

A key feature is handling custom Plate elements that lack standard Markdown representation, such as underline and mentions. @platejs/markdown converts these to MDX elements during serialization and parses them back during deserialization.

Example: Handling a custom date element.

Plate Node Structure:

{
  children: [
    { text: 'Today is ' },
    {
      children: [{ text: '' }],
      date: '2025-03-31',
      type: 'date',
    },
  ],
  type: 'paragraph',
}
{
  children: [
    { text: 'Today is ' },
    {
      children: [{ text: '' }],
      date: '2025-03-31'




Feature plugin codec:

@platejs/core includes the Markdown codec authoring types. Feature plugins declare their codecs without importing the optional Markdown runtime. When one plugin owns both HTML and Markdown codecs, declare both format keys in one defineCodecs object. Each format keeps its schema-specific inference.

lib/plate-editor.ts
import { defineBasePlugin } from '@platejs/core';
import { property } from '@platejs/plite';
 
export const BaseDatePlugin = defineBasePlugin('date', {
  codecs: ({ defineCodecs, schema: { type } }) =>
    defineCodecs({
      'text/html': {
        decode: ({ element }) => ({ date: element.dataset.date }),
        encode: ({ node }) => ({
          attributes: { 'data-date': node.date },
          tag: 'span',
        }),
        match: [{ attributes: { 























Conversion Process:

  1. Serialization (Plate → Markdown): The Plate date node writes as <date value="2025-03-31" />.
  2. Deserialization (Markdown → Plate): Both <date value="2025-03-31" /> and <date>2025-03-31</date> convert back to the Plate date node.

API Reference

MarkdownPluginState

Pass Markdown conversion behavior through MarkdownPlugin.configure({ initialState }).

OptionsMarkdownPluginState

    Whitelist specific node types (Plate types and Markdown AST types like strong). Cannot be used with disallowedNodes. If set, only listed types are processed. Default: null (all allowed).

    Blacklist specific node types. Cannot be used with allowedNodes. Listed types are filtered out. Default: null.

    Fine-grained node filtering with custom functions, applied after allowedNodes/disallowedNodes. - deserialize?: (mdastNode: any) => boolean: Filter for Markdown → Plate. Return true to keep. - serialize?: (node: Descendant) => boolean: Filter for Plate → Markdown. Return true to keep.

    Array of remark plugins (e.g., remark-gfm, remark-math, remark-mdx). Operates on Markdown AST (mdast). Default: [].

    Options passed to remark-stringify. Default: null.

    Marks serialized as plain text instead of Markdown formatting. Default: null.

MarkdownPlugin

MarkdownPlugin exposes conversion methods through editor.api.markdown and these product codecs:

FormatDecodeEncodeRepresentation
text/markdownYesYesClosed primary-content slice
text/plainYesNoClosed primary-content slice

The generic clipboard pipeline prefers application/x-plite-fragment for an exact Plate slice and delegates to these external formats when needed.


editor.api.markdown.deserialize

Converts a Markdown string into an EditorDocumentValue.

Parameters

    The Markdown string to deserialize.

    Options for this call, overriding the shared Markdown configuration.

OptionsDeserializeMdOptions

    Override plugin allowedNodes.

    Override plugin disallowedNodes.

    Override plugin allowNode.

    Override compiled feature codecs for this deserialization call.

    Override plugin remarkPlugins.

    If true, single line breaks (\\n) in paragraphs become paragraph breaks. Default: false.

    If true, skips the MDX preprocessing pass and filters remarkMdx out of the plugin list. Default: false.

    Preserves empty paragraph nodes during deserialization.

    Receives parser errors before the safe fallback path runs.

ReturnsEditorDocumentValue

    A document containing the deserialized primary block children. Markdown does not create named roots.


editor.api.markdown.deserializeInline

Converts inline Markdown text into Plite children.

Parameters

    Inline Markdown text to deserialize.

    Options for this call, overriding the shared Markdown configuration.

ReturnsDescendant[]

    Plite children for inline content.

editor.api.markdown.serialize

Converts an EditorDocumentValue into a Markdown string.

Parameters

    Options for this call, overriding the shared Markdown configuration.

OptionsSerializeMdOptions

    Plate document to serialize. Defaults to editor.read.value(). Only value.children is represented; named roots are not serialized.

    Override plugin allowedNodes.

    Override plugin disallowedNodes.

    Override plugin allowNode.

    Override compiled feature codecs for this serialization call.

    Override plugin remarkPlugins (affects stringification).

    Options passed to remark-stringify. Defaults to the shared Markdown configuration, with Plate setting emphasis to _ and resource links to false.

    Marks to serialize as plain text instead of Markdown formatting.

    Controls spread formatting for list output. Default: false.

    Preserves empty paragraph nodes during serialization.

    Serializes IDs from ElementIdPlugin as <block id="...">content</block>. The editor must install ElementIdPlugin. Deserialization restores each wrapper's persisted ID.

    • Default: false

Returnsstring

    A Markdown string.

parseMarkdownBlocks

Utility to parse a Markdown string into block-level tokens.

Parameters

    The Markdown string.

    Parsing options.

OptionsParseMarkdownBlocksOptions

    Marked token types (e.g., 'space') to exclude. Default: ['space'].

    Trim trailing whitespace from input. Default: true.

ReturnsToken[]

    Array of marked Token objects with raw Markdown.

Examples

Using a Remark Plugin (GFM)

Add support for GitHub Flavored Markdown: tables, strikethrough, task lists, and autolinks.

Plugin Configuration:

lib/plate-editor.ts
import { createPlateEditor } from 'platejs/react';
import { MarkdownPlugin } from '@platejs/markdown';
import remarkGfm from 'remark-gfm';
// Import Plate plugins for GFM elements
import { TablePlugin } from '@platejs/table/react';
import { TodoListPlugin } from '@platejs/list-classic/react'; // Ensure this is the correct List plugin for tasks
import { StrikethroughPlugin } from '@platejs/basic-nodes/react';
import { LinkPlugin } from '@platejs/link/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...other plugins
    TablePlugin,
    TodoListPlugin, // Or your specific task list plugin
    StrikethroughPlugin,
    LinkPlugin,
    MarkdownPlugin.configure({
      initialState: {
        remarkPlugins: [remarkGfm],
      },
    }),
  ],
});
lib/plate-editor.ts
import { createPlateEditor } from 'platejs/react';
import { MarkdownPlugin } from '@platejs/markdown';
import remarkGfm from 'remark-gfm';
// Import Plate plugins for GFM elements
import { TablePlugin } from '@platejs/table/react';
import { TodoListPlugin } from '@platejs/list-classic/react'; // Ensure this is the correct List plugin for tasks
import { StrikethroughPlugin } from '@platejs/basic-nodes/react';
import { LinkPlugin } from '@platejs/link/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...other plugins
    TablePlugin,
    TodoListPlugin, // Or your specific task list plugin
    StrikethroughPlugin,







Usage:

const markdown = `
A table:
 
| a | b |
| - | - |
 
~~Strikethrough~~
 
- [x] Task list item
 
Visit https://platejs.org
`;
 
// Assuming `editor` is your configured Plate editor instance
const document = editor.api.markdown.deserialize(markdown);
editor.update((tx) => {
  tx.value.replace(document);
});
 
const markdownOutput = editor.api.markdown.serialize();
// markdownOutput will contain GFM syntax

Customizing Rendering (Syntax Highlighting)

This example shows two approaches: customizing the rendering component for UI changes and customizing one conversion operation for a different Plate shape.

Background:

  • @platejs/markdown converts Markdown fenced code blocks (e.g., ```js ... ```) to Plate codeBlock elements with codeLine children.
  • The Plate CodeBlockElement (often from @platejs/code-block/react) renders this structure.
  • Syntax highlighting comes from CodeHighlightPlugin and a library like lowlight. See Code Block Plugin for details.

Approach 1: Customizing Rendering Component (Recommended for UI)

To change how code blocks appear, configure the CodeBlockPlugin descriptor.

components/my-editor.tsx
import { createPlateEditor } from 'platejs/react';
import {
  CodeBlockPlugin,
  CodeHighlightPlugin,
  CodeLinePlugin,
} from '@platejs/code-block/react';
import { MarkdownPlugin } from '@platejs/markdown';
import { MyCustomCodeBlockElement } from './my-custom-code-block'; // Your custom component
 
const editor = createPlateEditor({
  plugins: [
    CodeBlockPlugin.configure({ component: MyCustomCodeBlockElement }),
    CodeLinePlugin.configure({ component: MyCustomCodeLineElement }),
    CodeHighlightPlugin.configure({ component: MyCustomCodeSyntaxElement }),
    MarkdownPlugin,
    // ... other plugins




Refer to the Code Block Plugin documentation for complete examples.

Approach 2: One-operation conversion override (Advanced)

To alter the Plate JSON for one import, pass a rules override to that operation. Reusable code-block behavior belongs in the code-block feature plugin's text/markdown codec.

lib/plate-editor.ts
import { CodeBlockPlugin } from '@platejs/code-block/react';
 
const codeBlock = editor.plugin(CodeBlockPlugin);
 
const value = editor.api.markdown.deserialize(markdown, {
  rules: {
    [codeBlock.name]: {
      deserialize: (mdastNode) => ({
        children: [{ text: '' }],
        language: mdastNode.lang ?? undefined,
        rawCode: mdastNode.value || '',
        type: codeBlock.schema.type,
      }),
    },
  },
});
 
const output = editor.api.markdown.










Choose based on whether you're changing UI (Approach 1) or data structure (Approach 2).

Using Remark Plugins for Math (remark-math)

Enable TeX math syntax ($inline$, $$block$$).

Plugin Configuration:

lib/plate-editor.ts
import { createPlateEditor } from 'platejs/react';
import { MarkdownPlugin } from '@platejs/markdown';
import remarkMath from 'remark-math';
// Import Plate math plugins for rendering
import {
  EquationPlugin,
  InlineEquationPlugin,
} from '@platejs/math/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...other plugins
    EquationPlugin,
    InlineEquationPlugin,
    MarkdownPlugin.configure({
      initialState: {
        remarkPlugins: [remarkMath],
        // The installed math plugins own the `math` and `inlineMath` codecs.
      },


Usage:

const markdown = `
Inline math: $E=mc^2$
 
Block math:
$$
\\int_a^b f(x) dx = F(b) - F(a)
$$
`;
 
// Assuming `editor` is your configured Plate editor instance
const document = editor.api.markdown.deserialize(markdown);
// document.children contains 'inlineEquation' and 'equation' nodes.
 
const markdownOutput = editor.api.markdown.serialize({ value: document });
// markdownOutput will contain $...$ and $$...$$ syntax.














Using Mentions (remarkMention)

Enable mention syntax using the link format for consistency and special character support.

Plugin Configuration:

lib/plate-editor.ts
import { createPlateEditor } from 'platejs/react';
import {
  MarkdownPlugin,
  remarkMention,
} from '@platejs/markdown';
import { MentionPlugin } from '@platejs/mention/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...other plugins
    MentionPlugin,
    MarkdownPlugin.configure({
      initialState: {
        remarkPlugins: [remarkMention],
      },
    }),
  ],
});

Supported Format:

const markdown = `
Mention: [Alice](mention:alice)
Mention with spaces: [John Doe](mention:john_doe)
Full name with ID: [Jane Smith](mention:user_123)
`;
 
// Assuming `editor` is your configured Plate editor instance
const value = editor.api.markdown.deserialize(markdown);
// Creates mention nodes with appropriate values and display text
 
const markdownOutput = editor.api.markdown.serialize({ value });
// All mentions use the link format: [Alice](mention:alice), [John Doe](mention:john_doe), etc.











The remarkMention plugin uses the display text format - a Markdown link-style format that supports spaces and custom display text.

When serializing, all mentions use the link format to ensure consistency and support for special characters.

Using Columns

Enable column layouts with MDX support for multi-column documents.

Plugin Configuration:

lib/plate-editor.ts
import { createPlateEditor } from 'platejs/react';
import {
  MarkdownPlugin,
  remarkMdx,
} from '@platejs/markdown';
import { ColumnPlugin, ColumnItemPlugin } from '@platejs/layout/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...other plugins
    ColumnPlugin,
    ColumnItemPlugin,
    MarkdownPlugin.configure({
      initialState: {
        remarkPlugins: [remarkMdx], // Required for column MDX syntax
      },
    }),
  ],
});

Supported Format:

const markdown = `
<columnGroup>
  <column width="50%">
    Left column content with 50% width
  </column>
  <column width="50%">
    Right column content with 50% width
  </column>
</columnGroup>
 
<columnGroup>
  <column width="33%">First</column>
  <column width="33%">Second</column>
  <column width="34%">Third</column>
</columnGroup>
`;
 
// Assuming `editor` is your configured Plate editor instance
const value = editor.api.markdown.deserialize(markdown);
// Creates a columnGroup node with nested column elements
 
const markdownOutput = editor.api.markdown.serialize({ value });
// Preserves column structure with width attributes

Column Features:

  • Supports arbitrary number of columns
  • Width attributes are optional (defaults to equal distribution)
  • Nested content fully supported within columns
  • Width normalization ensures columns always sum to 100%

Remark Plugins

@platejs/markdown leverages the unified / remark ecosystem. Extend its capabilities through the remarkPlugins option. These plugins operate on the mdast (Markdown Abstract Syntax Tree).

Finding Plugins:

  • List of remark plugins (Official)
  • remark-plugin topic on GitHub
  • Awesome Remark

Common Uses:

  • Syntax Extensions: remark-gfm (tables, etc.), remark-math (TeX), remark-frontmatter, remark-mdx.
  • Linting/Formatting: remark-lint (often separate tooling).
  • Custom Transformations: Custom plugins to modify mdast.
Remark vs. Rehype

Plate components (e.g., TableElement, CodeBlockElement) render Plate JSON. remarkPlugins modify the Markdown AST. Unlike some renderers, rehypePlugins (for HTML AST) are not part of MarkdownPlugin's conversion pipeline. Run HTML transforms before Plate, or model controlled HTML-like content as MDX plus a feature-owned Markdown codec.

Syntax Support

@platejs/markdown uses remark-parse, adhering to CommonMark. Enable GFM or other syntaxes via remarkPlugins.

  • Learn Markdown: CommonMark Help
  • GFM Spec: GitHub Flavored Markdown Spec

Architecture Overview

@platejs/markdown bridges Markdown strings and Plate's editor format using the unified/remark ecosystem.

                                             @platejs/markdown
          +--------------------------------------------------------------------------------------------+
          |                                                                                            |
          |  +-----------+        +----------------+        +---------------+      +-----------+       |
          |  |           |        |                |        |               |      |           |       |
 markdown-+->+ remark    +-mdast->+ remark plugins +-mdast->+ mdast-to-plate+----->+   nodes   +-plate-+->react elements
          |  |           |        |                |        |               |      |           |       |
          |  +-----------+        +----------------+        +---------------+      +-----------+       |
          |       ^                                                                      |             |
          |       |                                                                      v             |
          |  +-----------+        +----------------+        +---------------+      +-----------+       |
          |  |           |        |                |        |               |      |           |       |
          |  | stringify |<-mdast-+ remark plugins |<-mdast-+ plate-to-mdast+<-----+ serialize |       |
          |  |           |        |                |        |               |      |           |       |
          |  +-----------+        +----------------+        +---------------+      +-----------+       |
          |                                                                                            |
          +--------------------------------------------------------------------------------------------+

Key Steps:

  1. Parse (Deserialization):
    • Markdown string → remark-parse → mdast.
    • remarkPlugins transform mdast (e.g., remark-gfm).
    • Markdown compiles intrinsic rules and codecs from the installed feature plugins, then converts mdast to Plate nodes.
    • Plate renders nodes via its component system.
  2. Stringify (Serialization):
    • Plate nodes → compiled feature codecs → mdast.
    • remarkPlugins transform mdast.
    • remark-stringify converts mdast to Markdown string.
Comparison with react-markdown
  • Direct Node Rendering: Plate directly renders its nodes via components, unlike react-markdown which often uses rehype to convert Markdown to HTML, then to React elements. - Bidirectional: Plate's Markdown processor is fully bidirectional. - Rich Text Integration: Nodes are integrated with Plate's editing capabilities. - Plugin System: Components are managed via Plate's plugin system.

Migrating from react-markdown

Migrating involves mapping react-markdown concepts to Plate's architecture.

Key Differences:

  1. Rendering Pipeline: react-markdown (MD → mdast → hast → React) vs. @platejs/markdown (MD ↔ mdast ↔ Plate JSON; Plate components render Plate JSON).
  2. Component Customization:
    • react-markdown: components prop replaces HTML tag renderers.
    • Plate:
      • Feature plugin codecs: Customize reusable mdast ↔ Plate JSON conversion.
      • Operation rules: Override conversion for one API call.
      • createPlateEditor components: Customize React components for Plate node types. See Appendix C.
  3. Plugin Ecosystem: @platejs/markdown uses remarkPlugins. rehypePlugins are not part of its conversion pipeline.

Mapping Options:

The option examples use import { MarkdownPlugin } from '@platejs/markdown'.

react-markdown Prop@platejs/markdown Equivalent/ConceptNotes
children (string)Pass to editor.api.markdown.deserialize(string)Input for deserialization; often in createPlateEditor initialValue.
remarkPluginsMarkdownPlugin.configure({ initialState: { remarkPlugins: [...] } })Operates on mdast.
rehypePluginsNot part of MarkdownPlugin's conversion pipeline.Run any HTML pipeline before passing Markdown or Plate nodes to Plate.
components={{ h1: MyH1 }}createPlateEditor({ components: { h1: MyH1 } })Configures the component for the default h1 persisted element type.
components={{ code: MyCode }}Feature plugin text/markdown codec plus components: { ['codeBlock']: MyCode }The codec owns mdast ↔ Plate conversion; the component owns rendering.
allowedElementsMarkdownPlugin.configure({ initialState: { allowedNodes: [...] } })Filters nodes during conversion (mdast/Plate types).
disallowedElementsMarkdownPlugin.configure({ initialState: { disallowedNodes: [...] } })Filters nodes during conversion.
unwrapDisallowedNo direct equivalent. Filtering removes nodes.A one-operation rules override can implement unwrapping.
skipHtmlDefault behavior strips most HTML.Sanitize or convert raw HTML before calling editor.api.markdown.deserialize.
urlTransformConfigure the link feature codec, or pass a one-operation rules override.Reusable policy belongs with the feature.
allowElementMarkdownPlugin.configure({ initialState: { allowNode: { ... } } })Function-based filtering during conversion.

Appendix A: HTML in Markdown

By default, @platejs/markdown does not process raw HTML tags. Standard Markdown syntax still becomes Plate nodes, but literal HTML like <div> is ignored unless you handle it outside Plate or model it as MDX/custom nodes.

MarkdownPlugin runs remark-parse, configured remarkPlugins, and Plate conversion rules. It does not run a rehype HTML stage, so rehype-raw and rehype-sanitize are not MarkdownPlugin state.

For raw HTML from a trusted source, convert it in your own content pipeline before calling Plate. For untrusted input, sanitize with a strict element and attribute whitelist before deserializing.

const safeMarkdown = await sanitizeMarkdownBeforePlate(untrustedMarkdown);
 
const value = editor.api.markdown.deserialize(safeMarkdown);
const safeMarkdown = await sanitizeMarkdownBeforePlate(untrustedMarkdown);
 
const value = editor.api.markdown.deserialize(safeMarkdown);

For HTML-like custom nodes that you control, prefer MDX syntax with remarkMdx and a codec on the owning feature plugin. That keeps the conversion in Plate's supported Markdown pipeline.

Security Warning

Raw HTML can carry XSS payloads. Treat untrusted Markdown as unsafe until your own pipeline sanitizes it with a strict element and attribute whitelist.

Appendix B: Conversion Ownership

Reusable conversion lives on the owning plugin under codecs['text/markdown']. MarkdownPlugin compiles codecs from installed plugins, so removing a feature also removes its conversion. Use rules on deserialize or serialize only when one operation needs a different result.

  • Plugin decode: from names the mdast node type or MDX tag. decode receives a typed node plus helpers for children, attributes, captions, and the owning plugin's resolved schema metadata.
  • Plugin encode: encode receives the schema-inferred Plate node plus helpers that guarantee phrasing, block, or flow content.
  • Operation decode override: rules keys use installed feature names, such as codeBlock and link.
  • Operation encode override: rules keys match the persisted node type.

Example: Overriding Link Deserialization

lib/plate-editor.ts
import { convertChildrenDeserialize } from '@platejs/markdown';
import { LinkPlugin } from '@platejs/link/react';
 
const link = editor.plugin(LinkPlugin);
 
editor.api.markdown.deserialize(markdown, {
  rules: {
    [link.name]: {
      deserialize: (node, decoration, options) => ({
        children: convertChildrenDeserialize(
          node.children,
          decoration,
          options
        ),
        customProp: 'this import only',
        title: node.title,
        type: link.schema.type,
        url: node.url,
      }),
    },
  },
});
lib/plate-editor.ts
import { convertChildrenDeserialize } from '@platejs/markdown';
import { LinkPlugin } from '@platejs/link/react';
 
const link = editor.plugin(LinkPlugin);
 
editor.api.markdown.deserialize(markdown, {
  rules: {
    [link.name]: {
      deserialize: (node, decoration, options) => ({
        children: convertChildrenDeserialize(
          node.children,
          decoration,
          options
        ),
        customProp: 'this import only',
        title: node.title,
        type: link.schema.type,
        url: node.url,
      }),


Installed feature codecs:

Owner packageMarkdown nodes
@platejs/markdownParagraphs, text, breaks, raw HTML
@platejs/basic-nodesHeadings, blockquotes, thematic breaks, basic marks
@platejs/linkLinks
@platejs/list, @platejs/list-classicIndent and classic lists
@platejs/code-block, @platejs/mathCode and math
@platejs/mediaImages, files, audio, video, embeds
@platejs/table, @platejs/layoutTables and columns
Feature packagesDates, mentions, footnotes, callouts, comments, suggestions, TOC

Emoji shortcodes: Add remark-emoji to remarkPlugins to turn :fire: into unicode 🔥 on deserialization and back to unicode on serialization.

GFM footnotes: With remark-gfm enabled, footnotes deserialize into footnoteReference and footnoteDefinition nodes. Add the matching Footnote plugins to render them as real editor nodes instead of falling back to unknown types.


Feature-owned MDX conversions (with remark-mdx):

MDX (mdast)Plate TypeNotes
<del>...</del>strikethrough (mark)Alt for ~~strikethrough~~
<sub>...</sub>script: 'sub' (mark)H2O
<sup>...</sup>script: 'sup' (mark)E=mc2
<u>...</u>underline (mark)Underlined
<mark>...</mark>highlight (mark)Highlighted
<span style="font-family: ...">fontFamily (mark)
<span style="font-size: ...">fontSize (mark)
<span style="font-weight: ...">fontWeight (mark)
<span style="color: ...">color (mark)
<span style="background-color: ...">backgroundColor (mark)
<date>...</date>dateCustom Date element
[text](mention:id)mentionCustom Mention element
<file name="..." />fileCustom File element
<audio src="..." />audioCustom Audio element
<video src="..." />videoCustom Video element
<toc />tocTable of Contents
<callout>...</callout>calloutCallout block
<columnGroup>...</columnGroup>columnGroupMulti-column layout container
<column width="50%">...</column>columnSingle column with optional width attribute

Appendix C: Components for Rendering

While feature codecs handle MD ↔ Plate conversion, Plate uses React components to render Plate nodes. Configure these in createPlateEditor via the components option or plugin component method.

Example:

components/my-editor.tsx
import { createPlateEditor, ParagraphPlugin, PlateLeaf } from 'platejs/react';
import { BoldPlugin } from '@platejs/basic-nodes/react';
import { CodeBlockPlugin } from '@platejs/code-block/react';
import { ParagraphElement } from '@/components/editor/paragraph'; // Example UI component
import { CodeBlockElement } from '@/components/editor/code-block'; // Example UI component
 
const editor = createPlateEditor({
  plugins: [
    ParagraphPlugin.configure({ component: ParagraphElement }),
    CodeBlockPlugin.configure({ component: CodeBlockElement }),
    BoldPlugin,
    /* ... */
  ],
});
components/my-editor.tsx
import { createPlateEditor, ParagraphPlugin, PlateLeaf } from 'platejs/react';
import { BoldPlugin } from '@platejs/basic-nodes/react';
import { CodeBlockPlugin } from '@platejs/code-block/react';
import { ParagraphElement } from '@/components/editor/paragraph'; // Example UI component
import { CodeBlockElement } from '@/components/editor/code-block'; // Example UI component
 
const editor = createPlateEditor({
  plugins: [
    ParagraphPlugin.configure({ component: ParagraphElement }),
    CodeBlockPlugin.configure({ component: CodeBlockElement }),
    BoldPlugin,
    /* ... */
  ],
});

Refer to Plugin Components for more on creating/registering components.

Appendix D: PlateMarkdown Component (Read-Only Display)

For a react-markdown-like component for read-only display:

components/plate-markdown.tsx
import React, { useEffect } from 'react';
import { Plate, PlateContent, usePlateEditor } from 'platejs/react';
import { MarkdownPlugin } from '@platejs/markdown';
// Import necessary Plate plugins for common Markdown features
import { BasicBlocksKit } from '@/components/editor/basic-blocks';
// ... include other plugins like BlockquotePlugin, CodeBlockPlugin, ListPlugin, etc.
// ... and mark plugins like BoldPlugin, ItalicPlugin, etc.
 
export interface PlateMarkdownProps {
  children: string; // Markdown content
  components?: Record<string, React.ComponentType<any>>; // Plate component overrides
  className?: string;
}
 
export function PlateMarkdown({
  children,
  components = {},
  className,
}: PlateMarkdownProps) {
  const editor = usePlateEditor({
    plugins: [
      // Include all plugins needed to render your Markdown
      ...BasicBlocksKit,
      MarkdownPlugin,
    ],
    components, // Pass through component overrides
  });
 
  useEffect(() => {
    editor.update((tx) => {
      tx.value.replace(editor.api.markdown.deserialize(children));
    });
  }, [children, editor]); // Re-deserialize when the Markdown changes
 
  return (
    <Plate editor={editor}>
      <PlateContent readOnly className={className} />
    </Plate>
  );
}
 
// Usage Example:
// const markdownString = "# Hello\nThis is *Markdown*.";
// <PlateMarkdown className="prose dark:prose-invert">
//   {markdownString}
// </PlateMarkdown>
components/plate-markdown.tsx
import React, { useEffect } from 'react';
import { Plate, PlateContent, usePlateEditor } from 'platejs/react';
import { MarkdownPlugin } from '@platejs/markdown';
// Import necessary Plate plugins for common Markdown features
import { BasicBlocksKit } from '@/components/editor/basic-blocks';
// ... include other plugins like BlockquotePlugin, CodeBlockPlugin, ListPlugin, etc.
// ... and mark plugins like BoldPlugin, ItalicPlugin, etc.
 
export interface PlateMarkdownProps {
  children: string; // Markdown content
  components?: Record<string, React.ComponentType<any>>; // Plate component overrides
  className?: string;
}
 































Initial Value

This PlateMarkdown component provides a read-only view. For full editing, see the Installation guides.

Security Considerations

@platejs/markdown prioritizes safety by converting Markdown to a structured Plate format, avoiding direct HTML rendering. However, security depends on:

  • Custom codecs and operation rules: Ensure decoders do not introduce unsafe data.
  • remarkPlugins: Vet third-party remark plugins for potential security risks.
  • Raw HTML Processing: Sanitize or convert raw HTML before passing Markdown to Plate. Treat untrusted Markdown as unsafe until your own pipeline has applied a strict whitelist.
  • Plugin Responsibility: URL validation in LinkPlugin (isUrl) or MediaEmbedPlugin (parseMediaUrl) is crucial.

Recommendation: Treat untrusted Markdown input cautiously. Sanitize if allowing complex features or raw HTML.

Related Links

  • remark: Markdown processor.
  • unified: Core processing engine.
  • MDX: JSX in Markdown.
  • react-markdown: Alternative React Markdown component.
  • remark-slate-transformer: Initial mdast ↔ Plate conversion work by inokawa.
[];
if (suggestion.installed) {
plainMarks.push(suggestion.schema.key);
}
if (comment.installed) {
plainMarks.push(comment.schema.key);
}
return {
initialState: {
plainMarks,
remarkPlugins: [
remarkMath,
remarkGfm,
remarkEmoji,
remarkMdx,
remarkMention,
],
},
};
}),
];
({
plugins: [
// ...otherPlugins,
...MarkdownKit,
],
});
remarkMdx,
remarkMention,
],
},
}),
],
});
lib/plate-editor.ts
import { createPlateEditor } from 'platejs/react';
import {
  MarkdownPlugin,
  remarkMention,
  remarkMdx,
} from '@platejs/markdown';
import remarkEmoji from 'remark-emoji';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
 
const editor = createPlateEditor({
  plugins: [
    // ...other Plate plugins
    MarkdownPlugin.configure({
      initialState: {
        remarkPlugins: [
          remarkMath,
          remarkGfm,
          remarkEmoji,
          remarkMdx,
          remarkMention,
        ],
      },
    }),
  ],
});
=>
editor.api.markdown.deserialize(markdownString),
});
components/my-editor.tsx
import { createPlateEditor } from 'platejs/react';
import { MarkdownPlugin } from '@platejs/markdown';
import { ItalicPlugin } from '@platejs/basic-nodes/react';
import { BasicBlocksKit } from '@/components/editor/basic-blocks';
// ... import other necessary Plate plugins for rendering elements
 
const markdownString = '# Hello, *Plate*!';
 
const editor = createPlateEditor({
  plugins: [
    // MarkdownPlugin must be included
    MarkdownPlugin,
    ...BasicBlocksKit,
    ItalicPlugin,
  ],
  // Use deserialize in the value factory for initial content
  initialValue: ({ editor }) =>
    editor.api.markdown.deserialize(markdownString),
});
const specificNodes = [
  {
    children: [{ text: 'Serialize just this paragraph.' }],
    type: 'paragraph',
  },
  {
    children: [{ text: 'And this heading.' }],
    type: 'heading', level: 1,
  },
];
 
// Assuming `editor` is your Plate editor instance
const partialMarkdownOutput = editor.api.markdown.serialize({
  value: { children: specificNodes },
});
console.info(partialMarkdownOutput);
,
type: 'date',
},
],
type: 'paragraph',
}
'data-date'
:
true
}, tag:
'span'
}],
},
'text/markdown': {
decode: ({ node, parseAttributes }) => ({
children: [{ text: '' }],
date: String(parseAttributes(node.attributes).value ?? ''),
type,
}),
encode: ({ node, propsToAttributes }) => ({
attributes: propsToAttributes({ value: node.date }),
children: [],
name: 'date',
type: 'mdxJsxTextElement',
}),
from: 'date',
kind: 'node',
},
}),
schema: {
element: {
properties: { date: property.string() },
void: 'inline',
},
},
});
lib/plate-editor.ts
import { defineBasePlugin } from '@platejs/core';
import { property } from '@platejs/plite';
 
export const BaseDatePlugin = defineBasePlugin('date', {
  codecs: ({ defineCodecs, schema: { type } }) =>
    defineCodecs({
      'text/html': {
        decode: ({ element }) => ({ date: element.dataset.date }),
        encode: ({ node }) => ({
          attributes: { 'data-date': node.date },
          tag: 'span',
        }),
        match: [{ attributes: { 'data-date': true }, tag: 'span' }],
      },
      'text/markdown': {
        decode: ({ node, parseAttributes }) => ({
          children: [{ text: '' }],
          date: String(parseAttributes(node.attributes).value ?? ''),
          type,
        }),
        encode: ({ node, propsToAttributes }) => ({
          attributes: propsToAttributes({ value: node.date }),
          children: [],
          name: 'date',
          type: 'mdxJsxTextElement',
        }),
        from: 'date',
        kind: 'node',
      },
    }),
  schema: {
    element: {
      properties: { date: property.string() },
      void: 'inline',
    },
  },
});
LinkPlugin,
MarkdownPlugin.configure({
initialState: {
remarkPlugins: [remarkGfm],
},
}),
],
});
const markdown = `
A table:
 
| a | b |
| - | - |
 
~~Strikethrough~~
 
- [x] Task list item
 
Visit https://platejs.org
`;
 
// Assuming `editor` is your configured Plate editor instance
const document = editor.api.markdown.deserialize(markdown);
editor.update((tx) => {
  tx.value.replace(document);
});
 
const markdownOutput = editor.api.markdown.serialize();
// markdownOutput will contain GFM syntax
],
});
// MyCustomCodeBlockElement.tsx would then implement the desired rendering
// (e.g., using react-syntax-highlighter), consuming props from PlateElement.
components/my-editor.tsx
import { createPlateEditor } from 'platejs/react';
import {
  CodeBlockPlugin,
  CodeHighlightPlugin,
  CodeLinePlugin,
} from '@platejs/code-block/react';
import { MarkdownPlugin } from '@platejs/markdown';
import { MyCustomCodeBlockElement } from './my-custom-code-block'; // Your custom component
 
const editor = createPlateEditor({
  plugins: [
    CodeBlockPlugin.configure({ component: MyCustomCodeBlockElement }),
    CodeLinePlugin.configure({ component: MyCustomCodeLineElement }),
    CodeHighlightPlugin.configure({ component: MyCustomCodeSyntaxElement }),
    MarkdownPlugin,
    // ... other plugins
  ],
});
 
// MyCustomCodeBlockElement.tsx would then implement the desired rendering
// (e.g., using react-syntax-highlighter), consuming props from PlateElement.
serialize
({
value,
rules: {
[codeBlock.schema.type]: {
serialize: (node) => ({
lang: node.language,
type: 'code',
value: node.rawCode,
}),
},
},
});
lib/plate-editor.ts
import { CodeBlockPlugin } from '@platejs/code-block/react';
 
const codeBlock = editor.plugin(CodeBlockPlugin);
 
const value = editor.api.markdown.deserialize(markdown, {
  rules: {
    [codeBlock.name]: {
      deserialize: (mdastNode) => ({
        children: [{ text: '' }],
        language: mdastNode.lang ?? undefined,
        rawCode: mdastNode.value || '',
        type: codeBlock.schema.type,
      }),
    },
  },
});
 
const output = editor.api.markdown.serialize({
  value,
  rules: {
    [codeBlock.schema.type]: {
      serialize: (node) => ({
        lang: node.language,
        type: 'code',
        value: node.rawCode,
      }),
    },
  },
});
}),
],
});
lib/plate-editor.ts
import { createPlateEditor } from 'platejs/react';
import { MarkdownPlugin } from '@platejs/markdown';
import remarkMath from 'remark-math';
// Import Plate math plugins for rendering
import {
  EquationPlugin,
  InlineEquationPlugin,
} from '@platejs/math/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...other plugins
    EquationPlugin,
    InlineEquationPlugin,
    MarkdownPlugin.configure({
      initialState: {
        remarkPlugins: [remarkMath],
        // The installed math plugins own the `math` and `inlineMath` codecs.
      },
    }),
  ],
});
const markdown = `
Inline math: $E=mc^2$
Block math:
$$
\\int_a^b f(x) dx = F(b) - F(a)
$$
`;
// Assuming `editor` is your configured Plate editor instance
const document = editor.api.markdown.deserialize(markdown);
// document.children contains 'inlineEquation' and 'equation' nodes.
const markdownOutput = editor.api.markdown.serialize({ value: document });
// markdownOutput will contain $...$ and $$...$$ syntax.
lib/plate-editor.ts
import { createPlateEditor } from 'platejs/react';
import {
  MarkdownPlugin,
  remarkMention,
} from '@platejs/markdown';
import { MentionPlugin } from '@platejs/mention/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...other plugins
    MentionPlugin,
    MarkdownPlugin.configure({
      initialState: {
        remarkPlugins: [remarkMention],
      },
    }),
  ],
});
const markdown = `
Mention: [Alice](mention:alice)
Mention with spaces: [John Doe](mention:john_doe)
Full name with ID: [Jane Smith](mention:user_123)
`;
// Assuming `editor` is your configured Plate editor instance
const value = editor.api.markdown.deserialize(markdown);
// Creates mention nodes with appropriate values and display text
const markdownOutput = editor.api.markdown.serialize({ value });
// All mentions use the link format: [Alice](mention:alice), [John Doe](mention:john_doe), etc.
lib/plate-editor.ts
import { createPlateEditor } from 'platejs/react';
import {
  MarkdownPlugin,
  remarkMdx,
} from '@platejs/markdown';
import { ColumnPlugin, ColumnItemPlugin } from '@platejs/layout/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...other plugins
    ColumnPlugin,
    ColumnItemPlugin,
    MarkdownPlugin.configure({
      initialState: {
        remarkPlugins: [remarkMdx], // Required for column MDX syntax
      },
    }),
  ],
});
const markdown = `
<columnGroup>
  <column width="50%">
    Left column content with 50% width
  </column>
  <column width="50%">
    Right column content with 50% width
  </column>
</columnGroup>
 
<columnGroup>
  <column width="33%">First</column>
  <column width="33%">Second</column>
  <column width="34%">Third</column>
</columnGroup>
`;
 
// Assuming `editor` is your configured Plate editor instance
const value = editor.api.markdown.deserialize(markdown);
// Creates a columnGroup node with nested column elements
 
const markdownOutput = editor.api.markdown.serialize({ value });
// Preserves column structure with width attributes
},
},
});
export function PlateMarkdown({
children,
components = {},
className,
}: PlateMarkdownProps) {
const editor = usePlateEditor({
plugins: [
// Include all plugins needed to render your Markdown
...BasicBlocksKit,
MarkdownPlugin,
],
components, // Pass through component overrides
});
useEffect(() => {
editor.update((tx) => {
tx.value.replace(editor.api.markdown.deserialize(children));
});
}, [children, editor]); // Re-deserialize when the Markdown changes
return (
<Plate editor={editor}>
<PlateContent readOnly className={className} />
</Plate>
);
}
// Usage Example:
// const markdownString = "# Hello\nThis is *Markdown*.";
// <PlateMarkdown className="prose dark:prose-invert">
// {markdownString}
// </PlateMarkdown>