VibeWeek
Home/Grow/Rich-Text Editor Implementation: Tiptap, Lexical, ProseMirror, Slate, BlockNote — Pick One Without Regretting It

Rich-Text Editor Implementation: Tiptap, Lexical, ProseMirror, Slate, BlockNote — Pick One Without Regretting It

⬅️ Day 6: Grow Overview

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:

  1. Format pick
  2. Storage strategy
  3. 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:

  1. Whitelist marks/nodes
  2. Strip inline styles on paste
  3. Convert headings to hierarchy that fits your design
  4. Drop unsupported elements (tables / iframes / etc.)
  5. 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:

  1. Whitelist
  2. Paste sanitization
  3. 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:

  1. Mention setup
  2. Slash command list
  3. 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:

  1. Upload to storage (S3 / Vercel Blob / etc.)
  2. Replace src with uploaded URL
  3. 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:

  1. Upload integration
  2. Loading state
  3. 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:

  1. Yjs / alternative pick
  2. Server hosting
  3. 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 inputmode attributes 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:

  1. A11y test plan
  2. Mobile test plan
  3. 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:

  1. Top 3 risks
  2. Mitigations
  3. 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