Tiptap Markdown: Import and Export (@tiptap/markdown)

September 11, 2026 · 9 min read

Tiptap Markdown: Import and Export with @tiptap/markdown

Tiptap Markdown support comes from @tiptap/markdown. It's the official extension that lets a Tiptap editor load a Markdown string and hand one back with editor.getMarkdown(). It arrived in Tiptap 3.7.0 and parses with MarkedJS. This tutorial installs it in a React app, round-trips a sample document, adds tables and task lists, and explains when the community and legacy options still apply.

Is Tiptap a Markdown Editor?

Not by default, and the distinction matters. Tiptap is a headless wrapper around ProseMirror that stores content as a JSON document and renders it as HTML. Typing # and getting a heading is an input rule, one of the "markdown shortcuts" the StarterKit extensions ship with. Those shortcuts don't make the editor speak Markdown; they only mimic the typing feel.

Real import and export is what the Tiptap Markdown extension, @tiptap/markdown, adds. Once it's installed, Tiptap can accept a Markdown string as content, serialize the current document back to Markdown, and let each extension declare how its node maps to Markdown tokens. The result is a WYSIWYG editor that reads and writes the same files a plain Markdown tool does.

That's why a Tiptap app usually keeps JSON or HTML as its storage format and treats Markdown as an exchange format. Our Markdown vs HTML comparison covers why rich editors lean on HTML internally.

Which Tiptap Markdown Option Should You Use in 2026?

Three packages show up when you search for Markdown support in Tiptap, and only one is the current recommendation.

OptionPackageParserStatus
Official extension@tiptap/markdownMarkedJSCurrent, shipped with Tiptap since 3.7.0; 3.31.3 at the time of writing
Community extensiontiptap-markdown (aguingand)markdown-it and prosemirror-markdownMaintainer recommends the official package; v0.9.0 targets Tiptap 3, v0.8 targets Tiptap 2
Legacy conversion@tiptap-pro/extension-import and extension-exportTiptap Cloud APIDeprecated; docs say it will be sunset in 2026

Use @tiptap/markdown for any new Tiptap 3 project. Use the community tiptap-markdown package only if you're pinned to Tiptap 2, where the official extension doesn't exist. Skip the Conversion extensions unless you already depend on them; they need a Tiptap Cloud App ID and a server-issued JWT for what the open-source package now does locally.

When to use the community tiptap-markdown package

The community extension by aguingand predates the official one and uses markdown-it for parsing and prosemirror-markdown for serializing. Its README states that Tiptap released an official Markdown extension in 3.7.0 and asks users to prefer it, and the maintainer doesn't plan to address open issues. Its last release, 0.9.0, requires @tiptap/core 3.0.1 or later.

If you're on Tiptap 2, it's still the practical choice. The API differs from the official package:

import { Markdown } from 'tiptap-markdown'

const editor = new Editor({
  extensions: [StarterKit, Markdown.configure({ html: true, tightLists: true, bulletListMarker: '-' })],
  content: '# Hello',
})

const md = editor.storage.markdown.getMarkdown()

Options such as transformPastedText and transformCopiedText let it treat pasted text as Markdown, which the official package doesn't do out of the box. Migrating later means swapping the import, replacing editor.storage.markdown.getMarkdown() with editor.getMarkdown(), and adding contentType: 'markdown' where you pass content.

Install @tiptap/markdown in a React App

You need @tiptap/react, @tiptap/starter-kit, and the Markdown package. The installation page shows the extension being added alongside StarterKit:

npm install @tiptap/react @tiptap/pm @tiptap/starter-kit @tiptap/markdown

Then create an editor whose initial content is Markdown. The contentType option tells Tiptap how to read the content string:

import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { Markdown } from '@tiptap/markdown'

const initial = `# Release notes

Version **2.1** ships two changes:

- Faster export
- A [changelog page](https://example.com/changelog)
`

export function Editor() {
  const editor = useEditor({
    extensions: [StarterKit, Markdown],
    content: initial,
    contentType: 'markdown',
  })

  return <EditorContent editor={editor} />
}

If you set contentType: 'markdown' without adding the extension, Tiptap treats the string as HTML and your headings show up as literal # characters, which is the most common first-run mistake.

How Do You Get Markdown Back Out of Tiptap?

The extension attaches a getMarkdown() method to the editor instance. Call it whenever you need the current document as a string, for example on save:

function SaveButton({ editor }) {
  return (
    <button onClick={() => {
      const md = editor.getMarkdown()
      console.log(md)
    }}>
      Save as Markdown
    </button>
  )
}

Loading new Markdown later uses the same contentType flag on the content commands:

editor.commands.setContent('## Replaced\n\nNew body text.', { contentType: 'markdown' })
editor.commands.insertContent('**appended** at the cursor', { contentType: 'markdown' })

If you also need HTML, nothing changes: editor.getHTML() and editor.getJSON() keep working, because the Markdown extension only adds a parser and serializer on top of the normal document model. That makes it easy to store JSON in your database, render HTML on a public page, and offer a Markdown download from the same editor state. The Markdown to HTML guide shows what a standalone converter produces if you want to compare.

Under the hood, editor.markdown.parse(md) turns a string into Tiptap JSON and editor.markdown.serialize(json) does the reverse. Both are public, so you can convert without touching the document, which is useful for previews and tests.

Round-tripping isn't byte-identical. The serializer normalises list markers, indentation, and blank lines to its own defaults. A document written with * bullets or four-space nesting may come back with a different marker and the configured indent width. The indentation option (style: 'space' or 'tab', size: 2 by default) controls that. In our experience it's better to store the serialized output as the canonical file than to diff it against the original.

Tables and Task Lists in Tiptap Markdown Export

StarterKit doesn't include tables or task lists, so the round trip drops them until you add the extensions. Both official packages define Markdown handlers, which is why the following works with no serializer code:

import { TableKit } from '@tiptap/extension-table'
import { TaskList, TaskItem } from '@tiptap/extension-list'

const editor = useEditor({
  extensions: [StarterKit, Markdown, TableKit, TaskList, TaskItem],
  content: `| Task | Owner |
|---|---|
| Ship export | Ana |

- [x] Write tests
- [ ] Update docs
`,
  contentType: 'markdown',
})

Tables come with a limit inherited from the format: a Markdown table cell can't hold nested blocks, so a cell with several paragraphs is flattened onto one line on export. The Markdown table guide covers the pipe syntax the serializer produces, and the checkbox guide shows the - [ ] form task items expect.

Here's the same sample in our editor. Compare what a plain GFM renderer does with what Tiptap produces after a round trip:

Release notes

Version 2.1 ships two changes:

Task Owner
Ship export Ana
Update docs Ravi
  • Write tests
  • Update docs

Tables need TableKit and task items need TaskList plus TaskItem before Tiptap will keep them.

58 words327 characters16 lines
Markdown

Custom Nodes and Streaming Markdown

Every Tiptap node or mark can declare how it parses from and renders to Markdown. The extension exposes createBlockMarkdownSpec and createInlineMarkdownSpec helpers for the common cases, and nodes can define their own parseMarkdown and renderMarkdown handlers, which is how the official table and task-item extensions do it. A node without a render handler has no Markdown representation, so if you build a callout or embed node, add the handler before you rely on getMarkdown().

Streaming AI output is the other frequent Tiptap Markdown question. Calling insertContent with each token works but produces flicker, because a half-received **bold is parsed as literal asterisks until the closing marker arrives. The approach we prefer is to accumulate the full string and call setContent with the accumulated text on each chunk. It costs a re-parse per chunk, which MarkedJS handles quickly for documents of a few thousand words, and the document is always in a valid state.

Raw HTML inside Markdown is parsed too, since the extension handles HTML embedded in a Markdown string. Keep that in mind if the Markdown comes from users.

Common Tiptap Markdown Mistakes

Passing Markdown without contentType. The editor assumes JSON or HTML, so your headings show up as literal # characters. Add contentType: 'markdown' to the editor options and to every setContent or insertContent call.

Expecting StarterKit to cover tables and checkboxes. They're separate packages. Without TableKit, TaskList, and TaskItem the parser has no node to map those tokens to, and the content is flattened or dropped.

Treating input rules as a Markdown mode. Typing - to start a list is an input rule from StarterKit and has nothing to do with @tiptap/markdown. Users who paste a Markdown file into a StarterKit-only editor get plain text, and that's expected.

@tiptap/markdown FAQ

With Tiptap Markdown support from @tiptap/markdown installed, a Tiptap editor reads Markdown in, exports it with editor.getMarkdown(), and keeps tables and task lists intact once their extensions are present. Prefer it over the community and legacy options for any Tiptap 3 project. To check what the exported file looks like in a plain renderer, paste it into the editor, or convert it with the Markdown to HTML tool and compare the markup.