@lexical/markdown
@lexical/markdown is the Markdown bridge for Meta's Lexical editor framework. It converts Markdown strings into Lexical nodes, serializes an editor or selection back to Markdown, and turns typed Markdown markers into rich-text formatting. You choose an ordered list of transformers, so the package can support the built-in headings, quotes, lists, fenced code, links, and text styles or an application-specific subset. It is an editor integration layer, not a general Markdown parser or renderer.
Use it when Lexical is already the editor and Markdown is a required input, output, or typing syntax. Do not install it as a general Markdown parser, and test your chosen transformer set for round-trip losses before making Markdown the source of truth.
Use it if
- Your application already uses Lexical and needs to load or save editor content as Markdown
- You want React or framework-free typing shortcuts such as turning '# ' into a heading and '**text**' into bold text
- You need control over the exact Markdown features an editor accepts through an explicit transformer list
- You want to export only the current Lexical selection instead of serializing the whole document
- You only need to parse Markdown to HTML or an abstract syntax tree: this package depends on Lexical and eight other @lexical packages, so markdown-it, marked, or remark-parse is a better fit
- You require full CommonMark or GitHub Flavored Markdown fidelity: the built-in transformer set does not include images or tables, and CHECK_LIST is exported but is not part of the default TRANSFORMERS array
- Lossless source round trips matter: import constructs editor nodes and export regenerates Markdown, so original spacing, marker choices, and other source-level details are not preserved
- Your project cannot keep Lexical packages on one version: version 0.49.0 pins lexical and every @lexical runtime dependency to exactly 0.49.0
- You use TypeScript older than 5.2: the published package declares TypeScript >=5.2 as a peer and routes older compilers to a declaration file that reports the incompatibility
Setup reality
Installation is not just `npm install @lexical/markdown`. Version 0.49.0 brings `lexical` plus eight @lexical runtime packages at the exact same version, and TypeScript 5.2 or newer is a peer requirement. In a real editor, every node class required by your chosen transformers must also appear in the Lexical editor configuration. Headings and quotes need the rich-text nodes, lists and check lists need `ListNode` and `ListItemNode`, links need `LinkNode`, and fenced code needs `CodeNode`. The shortcut registrar checks these dependencies and throws when a node is missing, so a transformer list copied from an example can fail at editor startup. React users install and render `MarkdownShortcutPlugin`; non-React users call `registerMarkdownShortcuts` and must run the returned cleanup function. Import and export helpers begin with `$`, which means they must execute inside `editor.update()`, an editor-state read callback, or the `editorState` initializer supplied to `LexicalComposer`. Import clears the target root by default and moves an existing selection to the start. The default transformer bundle is opinionated: it includes `==highlight==`, which is not standard Markdown, but excludes the separately exported `CHECK_LIST`. It also does not cover tables or images. Decide on one transformer array, register all of its node dependencies, and use that same array for import, shortcuts, and export or users can create formatting that does not survive saving.
Patterns
Install the Markdown package with Lexicalinstall-matching-version
npm install lexical@0.49.0 @lexical/markdown@0.49.0Keep all Lexical packages on the same release; @lexical/markdown 0.49.0 uses exact 0.49.0 dependencies.
Replace editor content with Markdownimport-markdown
import {
$convertFromMarkdownString,
TRANSFORMERS,
} from '@lexical/markdown';
editor.update(() => {
$convertFromMarkdownString(markdown, TRANSFORMERS);
});The helper clears the target root before importing and must run in a Lexical update or initialization context.
Serialize the editor to Markdownexport-markdown
import {
$convertToMarkdownString,
TRANSFORMERS,
} from '@lexical/markdown';
const markdown = editor.getEditorState().read(() =>
$convertToMarkdownString(TRANSFORMERS),
);Use the same transformer array for import and export; otherwise some imported node types may serialize as plain text or disappear.
Initialize LexicalComposer from Markdowninitialize-react-editor
import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {$convertFromMarkdownString, TRANSFORMERS} from '@lexical/markdown';
const initialConfig = {
namespace: 'ArticleEditor',
nodes: [HeadingNode, QuoteNode, ListNode, ListItemNode, LinkNode, CodeNode],
onError(error) { throw error; },
editorState: () => $convertFromMarkdownString(markdown, TRANSFORMERS),
};
<LexicalComposer initialConfig={initialConfig}>
<RichTextPlugin contentEditable={<ContentEditable />} ErrorBoundary={LexicalErrorBoundary} />
</LexicalComposer>Import runs during editor creation; register every node class required by the transformer array before it runs.
Enable Markdown typing shortcuts in Reactenable-react-shortcuts
import {TRANSFORMERS} from '@lexical/markdown';
import {MarkdownShortcutPlugin} from '@lexical/react/LexicalMarkdownShortcutPlugin';
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />This changes typed markers into rich nodes; it does not make the editor store Markdown internally.
Register shortcuts without Reactenable-vanilla-shortcuts
import {
registerMarkdownShortcuts,
TRANSFORMERS,
} from '@lexical/markdown';
const unregister = registerMarkdownShortcuts(editor, TRANSFORMERS);
// When the editor integration is disposed:
unregister();Keep and call the returned cleanup function or update listeners and command handlers remain registered.
Allow only a curated Markdown subsetchoose-transformer-subset
import {
BOLD_STAR,
HEADING,
ITALIC_STAR,
LINK,
QUOTE,
} from '@lexical/markdown';
const ARTICLE_TRANSFORMERS = [
HEADING,
QUOTE,
BOLD_STAR,
ITALIC_STAR,
LINK,
];Transformer order matters, especially for overlapping text markers; preserve the built-in ordering when selecting related formats.
Add task-list Markdown explicitlysupport-check-lists
import {CHECK_LIST, TRANSFORMERS} from '@lexical/markdown';
import {ListItemNode, ListNode} from '@lexical/list';
const nodes = [ListNode, ListItemNode];
const transformers = [CHECK_LIST, ...TRANSFORMERS];CHECK_LIST is exported separately but is not included in TRANSFORMERS; ListNode and ListItemNode must be registered.
Preserve source newlines during importpreserve-line-breaks
editor.update(() => {
$convertFromMarkdownString(
markdown,
TRANSFORMERS,
undefined,
true,
);
});The fourth argument preserves newlines; it disables adjacent-line merging and can produce different paragraph structure from normalized CommonMark.
Merge adjacent non-empty lines on importmerge-adjacent-lines
editor.update(() => {
$convertFromMarkdownString(
markdown,
TRANSFORMERS,
undefined,
false,
true,
);
});The fifth argument applies CommonMark-style line merging only when newline preservation is false.
Parse Markdown nodes without clearing the documentinsert-markdown-at-selection
import {$getSelection} from 'lexical';
import {$generateNodesFromMarkdownString, TRANSFORMERS} from '@lexical/markdown';
editor.update(() => {
const selection = $getSelection();
const nodes = $generateNodesFromMarkdownString(markdown, TRANSFORMERS);
selection?.insertNodes(nodes);
});$generateNodesFromMarkdownString returns detached nodes and leaves the document untouched until you insert them.
Export only selected contentexport-selection
import {$getSelection} from 'lexical';
import {
$convertSelectionToMarkdownString,
TRANSFORMERS,
} from '@lexical/markdown';
const selectedMarkdown = editor.getEditorState().read(() =>
$convertSelectionToMarkdownString(TRANSFORMERS, $getSelection()),
);A missing selection or collapsed range returns an empty string; it does not fall back to the whole document.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| markdown-it | npm | You need a standalone, extensible Markdown-to-HTML parser and are not using Lexical |
| marked | npm | You want a direct Markdown lexer and renderer with a small API outside an editor framework |
| remark-parse | npm | You need a Markdown syntax tree that can pass through the unified plugin ecosystem |
| @lexical/html | npm | Your Lexical persistence or interoperability format is HTML rather than Markdown |