/
367
Sponsor

Markdown

Load, save, and paste markdown in a Tiptap editor. Tables, task lists, images, mentions, and embeds round-trip to markdown without losing data.

The Markdown extension wraps the official @tiptap/markdown extension. It lets you load markdown into the editor, get the content back as markdown, and, with pasteMarkdown, turn pasted markdown text into formatted content.

Default nodes and marks, GFM tables, task lists, images, and the editorcn Mention, YouTube, and Twitter nodes all round-trip without losing data, as long as the extension for each node (Table, TaskList and TaskItem, Image, Mention, YouTube, Twitter) is registered alongside Markdown.

Installation

$ pnpm dlx shadcn@latest add "https://editorcn.vercel.app/r/markdown.json"

This installs extensions/markdown with its toolbar, extensions/core/, extensions/ui/, the style at extensions/ui/style.css, and the @tiptap/markdown package.

To install the files manually instead, install these packages and copy the markdown, core, and ui folders from the package source:

$ pnpm add @tiptap/core @tiptap/react @tiptap/pm @tiptap/markdown lucide-react

Usage

Add Markdown to the extensions and set contentType: "markdown" to load a markdown string:

"use client";
 
import { useEditor } from "@tiptap/react";
import { StarterKit } from "@tiptap/starter-kit";
 
import { RichTextEditor } from "@/components/editor";
import { Markdown } from "@/components/extensions/markdown";
 
import "@/components/editor/style.css";
 
export function MyEditor({ markdown }: { markdown: string }) {
  const editor = useEditor({
    immediatelyRender: false,
    extensions: [StarterKit, Markdown.configure({ pasteMarkdown: true })],
    content: markdown,
    contentType: "markdown",
    onUpdate: ({ editor }) => save(editor.getMarkdown()),
  });
 
  return (
    <RichTextEditor editor={editor}>
      <RichTextEditor.Content />
    </RichTextEditor>
  );
}

Replace or insert markdown later with the contentType option:

editor.commands.setContent("# Hello\n\n- item", { contentType: "markdown" });
editor.commands.insertContent("**bold**", { contentType: "markdown" });

With @editorcn/block-editor

The block editor takes the same editor instance, so the setup is identical:

const editor = useEditor({
  immediatelyRender: false,
  extensions: [
    StarterKit,
    Markdown.configure({ pasteMarkdown: true }),
    SlashCommand.configure({ suggestion: getSlashCommandSuggestion() }),
  ],
  content: markdown,
  contentType: "markdown",
});
 
return <BlockEditor editor={editor} />;

Toolbar

MarkdownToolbar adds a button with three actions:

  • Import Markdown opens a file picker and replaces the document with the chosen .md file. Press Ctrl+Z / Cmd+Z to undo.
  • Export Markdown downloads the document as a .md file.
  • Copy as Markdown copies the document to the clipboard.

The button is disabled when the Markdown extension isn't added to the editor.

import { MarkdownToolbar } from "@/components/extensions/markdown/toolbar";
 
import "@/components/extensions/ui/style.css";
 
<RichTextEditor.Toolbar>
  <RichTextEditor.ControlsGroup>
    <MarkdownToolbar editor={editor} filename="post.md" />
  </RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>;
PropDefaultDescription
editortoolbar contextThe editor to read and write
filename"document.md"Name of the exported file
classNameExtra classes for the button

Rename the menu items with the importMarkdown, exportMarkdown, copyMarkdown, and markdown labels on ToolbarProvider.

Settings

pasteMarkdown

Converts pasted plain text that looks like markdown (headings, lists, quotes, code fences, tables, links, or **bold**-style marks) into formatted content. Off by default.

Pastes that carry rich HTML, such as copying from a web page, keep the default behavior. Pastes from VS Code are treated as markdown, and pasting inside a code block always inserts plain text.

Markdown.configure({ pasteMarkdown: true });

indentation and markedOptions

All @tiptap/markdown options are supported, such as the list indentation style and options passed to marked.

Markdown.configure({
  indentation: { style: "space", size: 4 },
  markedOptions: { gfm: true },
});

Output

Standard nodes use standard markdown. Tables and task lists use GFM syntax. editorcn's custom nodes use the Tiptap shortcode syntax so their attributes survive:

NodeMarkdown
Mention[mention id="3" label="Grace Hopper"]
YouTube:::youtube {src="https://youtu.be/dQw4w9WgXcQ" width="560" height="315"} :::
Twitter:::twitter {tweetId="123456789"} :::

Formatting without a markdown equivalent (text color, font size and family, alignment, underline, subscript, superscript) is dropped on export. Use HTML or JSON when you need to keep it.

To render markdown read-only, parse it with MarkdownManager and pass the JSON to the Static Renderer:

import { MarkdownManager } from "@tiptap/markdown";
 
const extensions = [StarterKit, Mention];
const json = new MarkdownManager({ extensions }).parse(markdown);
 
<StaticRenderer content={json} extensions={extensions} />;

Helpers

importMarkdownFile, downloadMarkdown, copyMarkdown

The functions behind the toolbar. Use them to build your own menu, such as slash commands in the block editor:

importMarkdownFile(editor);
downloadMarkdown(editor, "post.md");
await copyMarkdown(editor);

looksLikeMarkdown

The check pasteMarkdown uses to decide whether pasted text is markdown. Use it in your own paste or import handlers.

looksLikeMarkdown("- item"); // true
looksLikeMarkdown("a plain sentence"); // false

Try it online

Convert content in your browser with the free HTML to Markdown and Markdown to HTML converters.