Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelog
  1. Home
  2. Blog
  3. Component guides
  4. React Rich Text Editor: Tiptap, Markdown and Mentions
Auf Deutsch lesen
  1. Blog
  2. Component guides
  3. React Rich Text Editor: Tiptap, Markdown and Mentions
Component guides
Component guides

React Rich Text Editor: Tiptap, Markdown and Mentions

SR
Serban Rusu · Founder of Wingo UI
Oct 19, 2026 · 9 min read

On this page

0%
  1. Which React rich text editor framework should I pick?
  2. Can I build a Notion like editor in React?
  3. What props does a Tiptap React editor need?
  4. Should the editor store HTML, JSON or Markdown?
  5. How do I add mentions and image uploads?
  6. How do I render saved rich text without loading Tiptap?
  7. How does the editor behave on a phone?
  8. Is the editor accessible with a keyboard?
  9. Is there a shadcn rich text editor?
  10. What are the common pitfalls?
  11. How do I add it to my project?
  12. FAQ
    1. What is the best rich text editor for React?
    2. Is Tiptap free?
    3. How do I use Tiptap with Next.js?
    4. Should I store rich text as HTML, JSON or Markdown?
    5. Is the Wingo UI Rich Text Editor free?

TL;DR

Build a React rich text editor on Tiptap: it is MIT licensed, runs on ProseMirror and gives you Markdown shortcuts, mentions and a schema that only lets known nodes in. Pick one storage format (HTML, Tiptap JSON or Markdown) and render saved content with the same styles, then make the toolbar reachable on a phone keyboard. The Wingo UI Rich Text Editor wraps all of this in one component with a format prop.

Published Oct 19, 2026

You need a React rich text editor for notes, comments, product descriptions or a help center. The first version is quick: install Tiptap, add StarterKit, render a toolbar. Then the questions arrive. What do you store in the database? How do pasted images get uploaded? Why does iOS zoom into the text on focus? Where does the toolbar go when the phone keyboard covers half the screen? This guide answers those, compares the editor frameworks, and shows the props that matter in the Wingo UI Rich Text Editor, with code for loading, saving, mentions and uploads.

Which React rich text editor framework should I pick?

Pick Tiptap unless you have a reason not to. It wraps ProseMirror in a React API, it is MIT licensed, and its extensions cover what product editors need: lists, task lists, links, images, mentions and Markdown. Facts below are from each project's docs and repository, as of October 2026.

OptionBuilt onLicenseWhat you get
Tiptap (opens in a new tab)ProseMirrorMIT (editor and open source extensions)A headless editor: you build the toolbar and menus
Lexical (opens in a new tab)Its own engine, from MetaMITA headless framework with React bindings
Plate (opens in a new tab)SlateMITA plugin framework plus shadcn-style UI copied through the shadcn CLI
Novel (opens in a new tab)TiptapApache-2.0A Notion-style editor with AI autocompletion through OpenAI
Wingo UI Rich Text EditorTiptapPro licenseOne component: toolbar, bubble menu, mentions, uploads, counts, phone layout

Lexical is a good choice when you want Meta's engine and do not mind building more of the UI. Plate fits if you already think in Slate. Tiptap itself sells a Notion-like editor template (opens in a new tab) with a slash menu, block reordering, AI and real-time collaboration; its docs say production use needs at least the Start plan, which Tiptap's pricing page (opens in a new tab) lists at $49 a month billed annually or $59 monthly.

Can I build a Notion like editor in React?

Yes, for the three features a notion like editor react build usually starts with: Markdown typed straight into the page, @mentions, and images that paste and upload. The Rich Text Editor does all three. Type # for a heading, - for a list, [] for a checklist, > for a quote, three backticks for a code block or **word** for bold, and paste Markdown from a README to get it formatted.

It does not ship a slash command menu or block drag handles. If you need them, the extensions prop appends your own Tiptap extensions after the built-in ones, and Tiptap's open source Suggestion utility, which already powers the mentions here, is the usual base for a "/" menu. If you want the full Notion feel with AI and collaboration out of the box, Novel or Tiptap's paid template will get you there faster.

Try the shortcuts in the live editor below, then switch the Output tabs to see the same document as HTML, JSON and Markdown.

Type # and a space for a heading or [] for a checklist, type @ to mention a teammate, then switch the Output tabs between HTML, JSON and Markdown

What props does a Tiptap React editor need?

Three for the content: value (or defaultValue), onValueChange and format. A Tiptap React editor built by hand needs useEditor, EditorContent, the extension list and immediatelyRender: false for Next.js; this component sets those up for you.

bash
npx wingo-ui@latest add rich-text-editor
tsx
"use client";
import { useState } from "react";
import { RichTextEditor, type RichTextValue } from "@/components/ui/rich-text-editor";
export function ProductDescription() {
const [html, setHtml] = useState<RichTextValue>("<p>Handmade in Portland from <strong>full-grain leather</strong>.</p>");
return (
<RichTextEditor
label="Description"
value={html}
onValueChange={(next) => setHtml(next)}
placeholder="Describe the product…"
characterLimit={2000}
showCount="both"
locale="en-US"
/>
);
}

onValueChange fires 150ms after the last keystroke (updateDebounce), and at once when the editor loses focus, so serializing a long document never slows typing. An empty editor reports "" in the HTML and Markdown formats. Passing back the value it just emitted never moves the caret; passing a different value, such as a document loaded from the server, replaces the content.

The props you will reach for next:

  • toolbar: true shows DEFAULT_TOOLBAR, false hides it while keys and shortcuts keep working, or pass the items in order with "|" between groups.
  • toolbarPosition="bottom" for a comment box, variant (outline, soft, ghost) and size (sm, md, lg).
  • headingLevels (1 to 3 by default), characterLimit, minHeight and maxHeight.
  • readOnly renders the document with the same styles and no toolbar, with links opening in a new tab.
  • setApi hands you getHTML, getJSON, getMarkdown, getText, focus, clear, insertContent, isEmpty and the Tiptap editor itself.

Should the editor store HTML, JSON or Markdown?

Pick one and stick to it, because each format changes how you render, search and migrate. The format prop decides what value, defaultValue and onValueChange speak.

StoreGood forWatch out for
html (default)Web pages, emails, server rendering without TiptapSanitize it before you render it
jsonAn editor that is the only writer, migrations, custom renderersNeeds Tiptap's schema to render
markdownContent people or tools also read raw: docs, AI prompts, GitSome marks have no standard syntax

The usual question about a react rich text editor markdown setup is whether the round trip loses anything. Through the official @tiptap/markdown extension, headings, lists, links, code and images map to standard Markdown, and task lists to the GitHub flavored - [ ] syntax. Underline has no Markdown syntax and comes out as ++text++, and a mention comes out as a shortcode like [@ id="emma-carter" label="Emma Carter"]. Tiptap's Markdown docs (opens in a new tab) call the extension an early release that can change. If you store Markdown, hide the underline button with a custom toolbar (Mod+U still applies underline, so expect the occasional ++text++) and test mentions against whatever reads the text later.

For plain form posts, name adds a hidden input with the value in the current format, so a Server Action receives it like any other field:

tsx
"use client";
import { RichTextEditor } from "@/components/ui/rich-text-editor";
import { Button } from "@/components/ui/button";
import { saveNote } from "./actions";
export function NoteForm() {
return (
<form action={saveNote} className="flex flex-col gap-3">
<RichTextEditor name="body" format="markdown" label="Note" required toolbar={["heading", "|", "bold", "italic", "link", "|", "bulletList", "taskList"]} />
<Button type="submit">Save note</Button>
</form>
);
}

Inside saveNote, formData.get("body") is the Markdown string. Because the editor emits on blur, a click on Save right after typing still sends the latest text.

This editor starts from Markdown (format="markdown"): format a line with a shortcut, then open the Markdown tab under it to see the source it would save
$ npx wingo-ui@latest add rich-text-editor
ProRich Text Editor docs

How do I add mentions and image uploads?

Pass mentions with an items function and onImageUpload with an upload function. Both can be async.

tsx
"use client";
import {
RichTextEditor,
type MentionItem,
type RichTextImageUploadOptions,
} from "@/components/ui/rich-text-editor";
async function searchPeople(query: string): Promise<MentionItem[]> {
const response = await fetch(`/api/people?q=${encodeURIComponent(query)}`);
return response.ok ? response.json() : [];
}
// xhr instead of fetch, because fetch reports no upload progress
function uploadImage(file: File, { onProgress, signal }: RichTextImageUploadOptions) {
return new Promise<string>((resolve, reject) => {
const xhr = new XMLHttpRequest();
const body = new FormData();
body.append("file", file);
xhr.open("POST", "/api/uploads");
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) onProgress(event.loaded / event.total);
};
xhr.onload = () => (xhr.status < 300 ? resolve(JSON.parse(xhr.responseText).url) : reject(new Error("Upload failed")));
xhr.onerror = () => reject(new Error("Upload failed"));
signal.addEventListener("abort", () => xhr.abort());
xhr.send(body);
});
}
export function TicketReply() {
return (
<RichTextEditor
label="Reply"
placeholder="Write a reply, @ to loop someone in"
mentions={{ items: searchPeople }}
onImageUpload={uploadImage}
imageAccept="image/png,image/jpeg,image/webp"
maxImageSize={5 * 1024 * 1024}
/>
);
}

Type @ and the list follows the caret. Arrows move, Enter or Tab picks, Escape closes, and the person becomes a chip with a data-id. An async items function waits 120ms between keystrokes, so you do not query the server on every letter. Use trigger for a different character and onMention to notify someone.

Images can come from the toolbar's file picker, a paste or a drop. A dimmed preview with a progress bar holds the spot while uploadImage runs; if it rejects, the placeholder disappears and a screen reader hears "The image could not be uploaded". Files over maxImageSize (10 MB by default) or outside imageAccept are refused before upload, and base64 images are not allowed into the document. Without onImageUpload the image button asks for an image address instead. If all you need is mentions in plain text, the lighter Mention Input stores them as id-based markup; the React mentions guide explains why ids beat names.

How do I render saved rich text without loading Tiptap?

Put the exported PROSE classes on the wrapper of your saved HTML, and the page matches the editor without shipping the editor. Sanitize the HTML on the server first. The editor's schema drops unknown tags, and the link editor refuses javascript: addresses through normalizeUrl, but anyone can POST any HTML to your API, so run it through a sanitizer such as DOMPurify before you store or render it.

tsx
import { PROSE } from "@/components/ui/rich-text-editor";
import { toneClasses } from "@/lib/tones";
import { cn } from "@/lib/utils";
export function NoteBody({ html }: { html: string }) {
// html was sanitized on the server before it was saved
return <div className={cn(PROSE, toneClasses.neutral)} dangerouslySetInnerHTML={{ __html: html }} />;
}

toneClasses sets the tone variables the MENTION chip classes read. PROSE covers headings, lists, quotes, code, links and images; checklists need their own styles here, or render the note with readOnly on the editor instead.

For stored Markdown, use the Markdown renderer. It renders GitHub flavored Markdown (tables, task lists, footnotes) in the same look, never renders raw HTML from the source, and has a streaming mode for AI answers, covered in the React Markdown streaming tutorial.

Saved Markdown rendered with the Markdown component: a table that scrolls sideways inside its own box when it is too wide, a numbered list and inline code
$ npx wingo-ui@latest add markdown
ProMarkdown docs

How does the editor behave on a phone?

It treats the phone keyboard as part of the layout. The text is 16px on phones at every size, so iOS Safari never zooms in on focus. While you type on a touch screen, the toolbar docks above the keyboard as a horizontally scrolling rail of 44px buttons, so formatting stays one thumb away. Below 640px the top toolbar keeps undo, redo, text style, bold, italic, link and the two lists, and moves the rest into a ⋯ menu. The text style menu, the ⋯ menu and the link editor open as bottom sheets.

The bubble menu over selected text only shows with a mouse or trackpad, because phones already have their own selection menus and a second one would cover them.

Is the editor accessible with a keyboard?

Yes. Toolbar buttons that have a shortcut show it in their tooltip (⌘B on a Mac, Ctrl+B elsewhere), and the pressed state follows the caret. Alt+F10 jumps from the text to the toolbar, Left and Right move between buttons, Home and End jump to the ends, and Escape returns to the text. Mod+K opens the link editor at the caret: Enter applies, an empty address removes the link, and addresses without a scheme get https://. Tab and Shift+Tab indent list items.

With label, description and error the editor uses the same Field anatomy as the other inputs, so aria-describedby, aria-invalid and aria-required are wired for you. The character counter speaks only when it crosses 90% and 100% of the limit, not on every keystroke. All strings live in labels and merge over DEFAULT_RICH_TEXT_EDITOR_LABELS, so you can translate the editor without forking it. The same field rules apply to every control in the React form components guide.

Is there a shadcn rich text editor?

Not in shadcn/ui itself: its component list (opens in a new tab) has a Textarea but no editor, as of October 2026. Community registries fill the gap. Minimal Tiptap (opens in a new tab) (MIT) installs through the shadcn CLI from its registry URL and outputs HTML, JSON, text or Markdown, and Plate installs its editor UI with npx shadcn@latest add @plate/plate-ui. The Wingo UI Rich Text Editor uses the same copy-the-source model. Pick Minimal Tiptap for a free toolbar on Tiptap, Plate for a Slate plugin stack, and this editor if you want mentions, uploads, counts and the phone toolbar already built.

What are the common pitfalls?

  • Rendering on the server. Tiptap needs immediatelyRender: false in Next.js, or the server and client HTML disagree. Set it in your own editor; the component already does.
  • Using a rich text editor for a plain field. A bio or a support message rarely needs headings. The free Textarea grows with its content and counts characters.
  • Mixing formats. Saving HTML from one screen and Markdown from another makes every later migration harder.
  • Comment boxes built from scratch. If you need replies, reactions and an optimistic composer, the Comment Thread already has them.

How do I add it to my project?

Run npx wingo-ui@latest add rich-text-editor. The CLI copies the source into components/ui, adds the components and hooks it imports, and installs the Tiptap packages, @radix-ui/react-popover, motion and lucide-react. The Rich Text Editor and the Markdown renderer are Pro components (pricing). More single-component deep dives are in the component guides category.

Components in this post

  • Rich Text Editor

    A React rich text editor on Tiptap with a toolbar, Markdown shortcuts, mentions, images and counts that outputs HTML, JSON or Markdown.

    Pro
  • Markdown

    A React Markdown renderer in the library's look: scrolling tables, highlighted code, task lists and a flicker-free streaming mode for AI.

    Pro

FAQ

What is the best rich text editor for React?

For most product editors, Tiptap: it is MIT licensed, built on ProseMirror and has React bindings, Markdown and mention extensions. Lexical from Meta is the main alternative, and Plate fits if you want a Slate-based framework with shadcn-style UI.

Is Tiptap free?

The Tiptap editor and its open source extensions, including StarterKit, Mention and Markdown, are MIT licensed. Tiptap's cloud features, such as hosted collaboration, are paid plans, and its Notion-like editor template needs at least the Start plan ($49 a month billed annually, as of October 2026) in production.

How do I use Tiptap with Next.js?

Put the editor in a client component and pass immediatelyRender: false to useEditor, as the Tiptap Next.js guide does, so the editor initializes on the client after hydration and avoids a mismatch. The Wingo UI Rich Text Editor already sets it and reserves its min height so nothing jumps on mount.

Should I store rich text as HTML, JSON or Markdown?

Store Tiptap JSON if the editor is the only writer, HTML if you mostly render it on web pages or in email, and Markdown if people or tools also read the raw text. Markdown cannot hold everything: underline and mentions come out as Tiptap-specific syntax.

Is the Wingo UI Rich Text Editor free?

No. The Rich Text Editor and the Markdown renderer are Pro components. The Textarea, which covers plain multi-line input, is free.

  • Rich text
  • Tiptap
  • Markdown
  • Forms
  • React

Share

SR

About the author

Serban Rusu

Founder of Wingo UI

Serban Rusu founded Wingo UI. He builds its component library, CLI and MCP server, and writes about React interfaces for phones and AI coding agents.

More from Serban
Keep reading

Related posts

All posts
Component guides

React Mentions: @People and #Tags That Survive Renames

React mentions done right: @people and #tags stored as id-based markup that survives renames, suggestions at the caret, and a list that works on phones.

SRSerban Rusu·Oct 19, 2026·9 min read
Building UI with AI agents

Nextjs llms.txt: Generate llms-full.txt and .md Pages

Build a nextjs llms.txt with App Router route handlers: an index, an llms-full.txt and a .md twin of every page, all generated from one content source.

SRSerban Rusu·Oct 16, 2026·10 min read
Building UI with AI agents

React Markdown Streaming Without Flicker or Scroll Jumps

React markdown streaming done right: repair half-typed syntax, re-parse only the last block, and keep the chat pinned to the bottom until the reader scrolls up.

SRSerban Rusu·Oct 16, 2026·10 min read
Newsletter

Get new posts by email

New guides, tutorials and comparisons from the Wingo UI blog, sent when they are published.

No spam. Unsubscribe at any time.

WingoUI

Animated, configurable, mobile-first React components and blocks. Copy the source, make it yours, and let your coding agent build with it.

Product

  • Components
  • Templates
  • Pricing
  • AI agents
  • Theme generator

Resources

  • Docs
  • Blog
  • Changelog
  • Changelog RSS
  • llms.txt

Legal

  • Terms
  • Privacy
  • Refunds
  • License
  • Imprint

Component categories

  • Buttons & Actions
  • Inputs
  • Forms
  • Navigation
  • Overlays
  • Feedback
  • Data Display
  • Tables & Lists
  • Charts & Stats
  • Layout
  • Media
  • AI Kit
  • Text & Effects
  • Mobile
  • Commerce
  • Marketing Sections
  • Blocks
  • Hooks & Utilities

© 2026 Wingo UI

Wingo UI