Rich-Text Editor Implementation: Tiptap, Lexical, ProseMirror, Slate, BlockNote — Pick One Without Regretting It
If your SaaS in 2026 lets users write more than a single line of text — comments, notes, descriptions, blog posts, knowledge base articles, replies — you'll need a rich-text editor. The naive choice (<textarea>) works until it doesn't: users want bold/italic/links; copy-paste from Google Docs breaks; markdown shortcuts demanded; mentions / collaborative editing requested. Picking the wrong editor library costs months — they're hard to migrate. Most indie SaaS picks one of: Tiptap (modern; mid-market default), Lexical (Meta-built; performance), ProseMirror (low-level; powerful), Slate (legacy popular), BlockNote (Notion-style blocks). The right pick depends on what users will produce, collaboration needs, and your tolerance for complexity.
A working rich-text editor implementation answers: which library (Tiptap / Lexical / BlockNote etc.), what features to ship (basic format / mentions / collaborative / blocks / AI), how to handle storage (HTML / Markdown / JSON), how to handle pasting (clean up Google-Docs HTML; strip styles), how to handle images (upload pipeline), how to handle accessibility (keyboard / screen-reader), how to handle mobile (touch issues), and how to handle realtime collaboration if needed.
This guide is the implementation playbook for rich-text editors. Companion to Real-time Collaboration, Image Upload & Processing Pipeline, Search Autocomplete & Typeahead, Form Validation UX, and Keyboard Shortcuts & Command Palette.
Why Editor Choice Matters
Get the failure modes clear first.
Help me understand editor failures.
The 8 categories:
**1. Wrong library lock-in**
Pick Slate; 6 months later realize lacks collaboration; migrate = rewrite all rendering / storage / mentions.
**2. Paste from Google Docs / Word**
User copies; paste includes 50 styles; UI breaks; data corrupted.
**3. Markdown / shortcut behavior**
User types `**bold**`; expects bold; gets literal asterisks. Or: types markdown; gets weird HTML.
**4. Image / file pasting**
User drops image; nothing happens. Or: image embedded as base64; bloats document.
**5. Mentions broken**
@-mention shows wrong list; submission breaks; or @ is just text.
**6. Mobile touch issues**
Selection misbehaves; toolbar overflows; iOS soft-keyboard interactions break.
**7. Accessibility broken**
Screen readers can't read content; keyboard-only users locked out.
**8. Performance with long documents**
Editor lags at 10K+ characters; no virtualization; freezes browser.
For my product:
- Editor surfaces (where used)
- Document length expected
- Collaboration needs
Output:
1. Top failure mode
2. Risk level
3. Library priority
The biggest unforced error: picking by trend. "Notion uses X" → use X. But Notion is a 100-engineer team; your context differs. Pick by your needs.
The 2026 Library Landscape
Help me understand options.
The main libraries in 2026:
**Tiptap (built on ProseMirror)**:
Pricing: Free OSS core; Tiptap Cloud (collaboration / AI features) paid.
Pros:
- Modern API; React-friendly
- ProseMirror-powered (battle-tested)
- Extension system; 100+ extensions
- Collaboration support (Y.js)
- AI features in cloud tier
- Most-used in 2026 for new projects
Cons:
- Some advanced features in cloud tier (paid)
- Learning curve for ProseMirror underneath
Best for: most new B2B SaaS; mid-market scale; need extensibility.
**Lexical (Meta)**:
Pricing: Free OSS.
Pros:
- Modern; built by Meta for FB Notes
- Excellent performance; small bundle
- Strong React integration
- Good docs
Cons:
- Newer; smaller ecosystem than Tiptap
- Less plugin variety
- Collaboration not as turnkey
Best for: performance-critical; React-only; can build extensions yourself.
**BlockNote**:
Pricing: Free OSS.
Pros:
- Notion-style block editor out of box
- Built on Tiptap (so all Tiptap extensions work)
- Beautiful default UX
- Modern; growing fast 2024-2026
Cons:
- Block-only paradigm (not flexible inline formatting)
- Younger; smaller community
Best for: Notion-style apps; want default beautiful UX.
**ProseMirror (low-level)**:
Pricing: Free OSS.
Pros:
- Maximum flexibility
- The foundation Tiptap / others build on
- Battle-tested at scale (Atlassian, Drupal, etc.)
Cons:
- Steep learning curve
- More code to write yourself
- React integration not built-in (use prosemirror-react or Tiptap on top)
Best for: highly custom requirements; willing to invest months.
**Slate**:
Pricing: Free OSS.
Pros:
- React-native (no DOM-manipulation library)
- Flexible; popular in early 2020s
Cons:
- Stagnant in 2025-2026 (less active dev)
- Migration off is hard
Best for: existing Slate codebase. New projects: Tiptap or Lexical instead.
**Quill**:
Pricing: Free OSS.
Pros:
- Simple; good default
- Long-standing
Cons:
- Older architecture
- Less flexible
- Maintenance lagging
Best for: simple use cases; legacy.
**Plate (Slate-based)**:
Pricing: Free OSS.
Pros:
- Slate + opinionated plugins
- React-native
Cons:
- Tied to Slate's trajectory
**Editor.js**:
Pricing: Free OSS.
Pros:
- Block-based
- Clean UI
Cons:
- Less popular in B2B SaaS
- Limited extensibility
**TinyMCE / CKEditor (commercial)**:
Pricing: Free / paid tiers.
Pros:
- Enterprise-grade
- Very feature-rich
- Used in WordPress / many CMSs
Cons:
- Heavy; not React-first
- Older feel
For my product:
- Use case complexity
- React-first?
- Collaboration?
Output:
1. Top 2 picks
2. Why
3. Migration plan if migrating
The 2026 default for new projects: Tiptap (or BlockNote if Notion-style blocks are core). Established ecosystem; modern; collaboration-ready. Go with Lexical only if performance is critical and you're React-only.
Picking Storage Format: HTML, Markdown, or JSON
Help me decide storage format.
The three options:
**Option 1: HTML**
Store: `<p><strong>bold</strong> text</p>`
Pros:
- Easy to render (just inject)
- Standard; portable
- SEO-friendly directly
Cons:
- HTML sanitization required (XSS risk)
- Hard to query / transform
- Heavier than JSON
- Specific tags / attributes vary by editor
Use case: blog content; CMS; simple needs.
**Option 2: Markdown**
Store: `**bold** text`
Pros:
- Lightweight
- Human-readable
- Easy to query / transform
- Portable (Markdown standard)
Cons:
- Editor must convert HTML ↔ Markdown
- Some features lose info (e.g. specific HTML attributes)
- Embedded images / mentions require extensions
Use case: documentation; comments; content meant to be exported.
**Option 3: JSON (ProseMirror / Lexical native format)**
Store:
```json
{
"type": "doc",
"content": [
{ "type": "paragraph", "content": [
{ "type": "text", "marks": [{"type":"bold"}], "text": "bold" },
{ "type": "text", "text": " text" }
]}
]
}
Pros:
- Lossless (preserves all editor features)
- Structured; queryable
- Round-trip safe (load → edit → save → reload = same)
- Rich features supported (mentions / blocks / embeds)
Cons:
- Renderable only via editor (or matching renderer)
- Larger than markdown / HTML
- Not directly SEO-friendly
Use case: rich documents; collaboration; future-proofing.
The 2026 recommendation:
For most B2B SaaS in 2026: JSON.
- Tiptap / Lexical / ProseMirror native
- Round-trip lossless
- Renderer can output HTML for SEO / rendering
Hybrid:
- Store JSON (canonical)
- Render HTML on save for read-only views (SEO / export)
- Index plain-text extract for search
For my use case:
- SEO needs
- Collaboration needs
- Export needs
Output:
- Format pick
- Storage strategy
- Rendering strategy
The pivotal decision: **JSON for editor; render to HTML for read-only / SEO**. Best of both. Use Tiptap's `getHTML()` or similar; cache rendered HTML.
## Pasting from Google Docs / Word (the Hardest Part)
Help me handle pastes.
The problem: user copies from Google Docs / Word / web. Paste includes inline styles, fonts, colors, weird tags. Naive paste = corrupted document.
The solutions:
1. Sanitize on paste
Tiptap, Lexical, ProseMirror have paste-handling plugins.
Tiptap:
import { Editor } from '@tiptap/react';
editor.setOptions({
editorProps: {
transformPastedHTML: (html) => {
// Strip inline styles
return html.replace(/style="[^"]*"/g, '');
},
},
});
2. Whitelist allowed marks / nodes
Configure editor to only accept:
- bold, italic, underline, strike
- headings (h1-h3)
- lists, blockquotes, code
- links, images
Drop everything else.
3. Convert to markdown on paste; back to JSON
Pasted HTML → Markdown (turndown.js) → JSON (parse markdown back to editor's format).
Strips fancy formatting; keeps structure.
4. Use library's paste handlers
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
const editor = new Editor({
extensions: [StarterKit.configure({
// Built-in paste handling
})],
});
StarterKit and equivalents have reasonable defaults.
The thorough fix:
- Whitelist marks/nodes
- Strip inline styles on paste
- Convert headings to hierarchy that fits your design
- Drop unsupported elements (tables / iframes / etc.)
- Test with: Google Docs, Word, Notion, Apple Notes, web pages
Maintain a paste test fixture; run through your editor; verify clean.
For my editor: [library]
Output:
- Whitelist
- Paste sanitization
- Test fixtures
The discipline: **test paste from 5+ sources**. Google Docs, Word, Notion, Apple Notes, plain HTML. Every source has quirks. Build a regression test from copies of each.
## Mentions, Slash Commands, and Custom Nodes
Help me ship mentions and custom nodes.
Mentions:
User types @; dropdown shows people/items; pick one; link inserted.
Tiptap mention extension:
import Mention from '@tiptap/extension-mention';
new Editor({
extensions: [
StarterKit,
Mention.configure({
suggestion: {
items: ({ query }) => searchUsers(query),
render: () => ({ /* ... */ }),
},
}),
],
});
UX:
- @ triggers dropdown
- Type to filter
- Arrow keys + enter
- Esc cancels
- Selected mention = atomic chunk (delete in one keystroke)
Slash commands (Notion-style):
User types /; dropdown of insert options (heading, list, image, code, etc.).
Tiptap suggestion extension OR @tiptap/suggestion + custom logic.
Custom nodes:
Embed something not built-in: Loom video, Figma frame, Stripe receipt, etc.
Define node:
import { Node } from '@tiptap/core';
const LoomEmbed = Node.create({
name: 'loomEmbed',
group: 'block',
atom: true,
parseHTML: () => [{ tag: 'loom-embed' }],
renderHTML: () => ['loom-embed', 0],
addNodeView: () => ReactNodeViewRenderer(LoomEmbedComponent),
});
Insert via slash command or paste handler.
For my product:
- Mention sources (users / docs / etc.)
- Custom nodes needed
Output:
- Mention setup
- Slash command list
- Custom nodes
The polish that delights: **context-aware mentions**. @-mentions show relevant items based on context (in a comment thread? show participants first; in a doc? show recent collaborators). Small detail; users notice.
## Image and File Handling
Help me handle images / files.
The flow:
User drags image OR pastes from clipboard OR uses image button.
// Tiptap Image extension
import Image from '@tiptap/extension-image';
editor.chain().focus().setImage({ src: '/uploaded/path.jpg', alt: 'Description' }).run();
But images need:
- Upload to storage (S3 / Vercel Blob / etc.)
- Replace src with uploaded URL
- Resize / optimize (image CDN handles)
Pattern:
async function handleImagePaste(file: File) {
// Show loading placeholder
const placeholder = editor.commands.insertContent({
type: 'image',
attrs: { src: 'placeholder.gif', uploading: true },
});
// Upload
const url = await uploadToBlob(file);
// Replace placeholder with real URL
editor.commands.updateAttributes('image', { src: url, uploading: false });
}
// Wire to paste / drop handlers
editor.setOptions({
editorProps: {
handlePaste: (view, event) => {
const items = Array.from(event.clipboardData?.items ?? []);
const imageItem = items.find(i => i.type.startsWith('image/'));
if (imageItem) {
handleImagePaste(imageItem.getAsFile()!);
return true;
}
return false;
},
},
});
See Image Upload & Processing Pipeline for upload pipeline.
File embeds (PDFs / docs):
Similar pattern; treat as atomic node:
- Upload
- Show preview / download link
- Block-level node
For my editor: [needs]
Output:
- Upload integration
- Loading state
- Error handling
The single most-impactful UX detail: **placeholder while uploading**. User sees blurred / loading image; replaces with real once uploaded. Without: user types after image; image inserts at wrong spot when upload completes.
## Real-time Collaboration (Y.js)
Help me ship collaboration.
If two+ users edit same document at same time, you need conflict-free editing.
The 2026 standard: Y.js + WebSocket / WebRTC transport.
Tiptap with Y.js:
import { Collaboration } from '@tiptap/extension-collaboration';
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor';
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
const ydoc = new Y.Doc();
const provider = new WebsocketProvider('wss://your-y-websocket-server', 'doc-id', ydoc);
const editor = new Editor({
extensions: [
StarterKit.configure({ history: false }), // Y.js handles history
Collaboration.configure({ document: ydoc }),
CollaborationCursor.configure({
provider,
user: { name: 'Alice', color: '#f783ac' },
}),
],
});
Server: y-websocket-server (Node) or Hocuspocus (Tiptap's managed Y.js server).
Hocuspocus (Tiptap Cloud):
- Managed Y.js server
- Authentication
- Persistence to your DB
- Rate limiting
- $$$ but saves weeks of building
Self-hosted Y.js:
- y-websocket reference server
- Persist to Postgres / Redis
- Auth integration
- Ops burden
Other collaboration options:
- LiveBlocks — managed; supports Y.js + custom protocols; popular alternative
- Replicache — different paradigm; sync-by-mutation
- Convex — backend with realtime sync built-in
For my needs:
- Realtime requirement
- Concurrent users expected
- Build vs buy
Output:
- Yjs / alternative pick
- Server hosting
- Cursor / awareness UX
The 2026 reality: **building realtime collaboration is hard**. Tiptap Cloud / Hocuspocus / LiveBlocks save you 1-3 months. Buy unless realtime is your core differentiator.
## Accessibility and Mobile
Help me handle a11y + mobile.
Accessibility:
- Keyboard nav: Tab moves between editor + toolbar; arrows in editor
- Screen readers: editor must announce content + formatting
- ARIA: editor must have role / labels
- Focus management: clear visual + programmatic focus
Most modern editors (Tiptap / Lexical) have decent defaults. Test:
- Tab into editor; type; verify announced
- Use keyboard shortcuts
- VoiceOver / NVDA test
Mobile:
iOS / Android quirks:
- Soft keyboard covers toolbar
- Selection / cursor placement different
- Auto-correct interferes
- IME composition (Chinese / Japanese / Korean input) breaks naive editors
Solutions:
- Move toolbar above keyboard (sticky to keyboard with viewport units)
- Test all major libraries on iOS Safari / Android Chrome
- Use
inputmodeattributes on input elements - IME: ensure editor doesn't fight it (Lexical / Tiptap handle)
Mobile-specific UX:
- Floating toolbar that follows selection
- Larger touch targets for toolbar buttons
- Long-press for context menu
For my editor: [test]
Output:
- A11y test plan
- Mobile test plan
- Adjustments
The single most-skipped test: **mobile**. Editor works on desktop; ship; users complain on iOS. Test on real devices; iOS Safari is finicky.
## Common Editor Mistakes
Help me avoid mistakes.
The 10 mistakes:
1. Picking by trend, not needs Tiptap because Notion-vibes; doesn't fit your use.
2. Storing HTML; later wishing for JSON Migration painful.
3. No paste sanitization Google Docs paste corrupts documents.
4. Naive image handling Base64-embedded images bloat document; can't optimize.
5. No mobile test Soft keyboard issues; selection bugs.
6. Custom-node nightmare Building 10 custom nodes when 2 would do.
7. Realtime built from scratch Take 3-6 months instead of buying $200/mo Hocuspocus.
8. Heavy bundle (1MB+ editor) First-load slow; consider lazy-load.
9. No undo / redo Default for most libraries; verify works.
10. No accessibility test Screen readers can't read; keyboard fails.
For my editor: [risks]
Output:
- Top 3 risks
- Mitigations
- Tests
The single most-painful mistake: **storing HTML when you need JSON later**. Mentions break; collaboration needs added; you migrate by parsing 100K stored documents. Plan for JSON storage from day 1 even if HTML works initially.
## What Done Looks Like
A working rich-text editor implementation:
- Library picked deliberately (Tiptap / Lexical / BlockNote / etc.)
- JSON storage; HTML rendered for read-only views
- Paste sanitization tested with Google Docs / Word / Notion / Apple Notes
- Mentions + slash commands working
- Image upload integrated with storage pipeline
- Loading state for uploads
- Mobile tested (iOS / Android)
- Accessibility tested (keyboard + screen reader)
- Realtime collaboration via Y.js (Hocuspocus / LiveBlocks if needed)
- Bundle size <300KB editor (lazy-load if larger)
- Undo / redo works
- No XSS surface (sanitize HTML on render)
The proof you got it right: a user pastes from Google Docs; the document looks right; @-mentions work; image drops upload smoothly; mobile experience matches desktop. No data corruption.
## See Also
- [Real-time Collaboration](real-time-collaboration-chat.md) — Y.js / collaboration backend
- [Image Upload & Processing Pipeline](image-upload-processing-pipeline-chat.md) — image upload integration
- [Search Autocomplete & Typeahead](search-autocomplete-typeahead-chat.md) — mention dropdown patterns
- [Form Validation UX](form-validation-ux-chat.md) — form-with-rich-text concerns
- [Keyboard Shortcuts & Command Palette](keyboard-shortcuts-command-palette-chat.md) — slash commands related
- [Performance Optimization](performance-optimization-chat.md) — editor bundle size impact
- [Internationalization](internationalization-chat.md) — IME composition for non-Latin scripts
- [Content Moderation Pipeline](content-moderation-pipeline-chat.md) — UGC moderation
- [Slugs & URL Handling](slugs-and-url-handling-chat.md) — companion content concern
- [VibeReference: React](https://vibereference.dev/frontend/react) — React patterns
- [VibeReference: TypeScript Patterns](https://vibereference.dev/frontend/typescript-patterns) — type safety in editors
- [VibeReference: Accessibility](https://vibereference.dev/product-and-design/accessibility) — broader a11y context