TL;DR
Tiptap is the default choice for most React projects in 2026 — it abstracts ProseMirror into a clean, extensible API with a thriving extension ecosystem. Lexical (Meta's editor framework) is the right choice for performance-critical applications and large React Native teams. Slate.js gives you maximum control but requires significant custom code. Quill is legacy — functional but no longer getting meaningful updates.
Key Takeaways
- Tiptap v2: ~1.2M weekly downloads — most popular React rich text editor, ProseMirror-based
- Lexical: ~1.1M weekly downloads — Meta's editor framework, built for Facebook/Messenger/Docs
- Slate.js: ~550K weekly downloads — unopinionated, plugin-first, steeper learning curve
- Quill: ~1.3M weekly downloads — largely legacy installs, limited modern TypeScript support
- Tiptap wins on developer experience and extension ecosystem
- Lexical wins on performance, React Native support, and Meta's production backing
- Don't use Quill for new projects — choose Tiptap or Lexical
Download Trends
| Package | Weekly Downloads | Backing | Bundle Size |
|---|---|---|---|
quill | ~1.3M | Community | ~400KB |
@tiptap/core | ~1.2M | Open-source + enterprise | ~56KB |
@lexical/react | ~1.1M | Meta | ~65KB |
slate | ~550K | Community | ~45KB |
Architecture Overview
| Library | Foundation | Philosophy |
|---|---|---|
| Tiptap | ProseMirror | Opinionated, extensions, React wrapper |
| Lexical | From scratch | Lean core, plugins, framework-agnostic |
| Slate.js | From scratch | Plugin-first, maximum control |
| Quill | Delta format | Simple API, opinionated toolbar |
Tiptap
Tiptap wraps ProseMirror (the most powerful rich text editing engine) with a developer-friendly API. ProseMirror's power without ProseMirror's complexity.
import { useEditor, EditorContent } from "@tiptap/react"
import StarterKit from "@tiptap/starter-kit"
import Image from "@tiptap/extension-image"
import Link from "@tiptap/extension-link"
import Placeholder from "@tiptap/extension-placeholder"
function RichTextEditor() {
const editor = useEditor({
extensions: [
StarterKit, // Heading, Bold, Italic, Lists, Code, Blockquote, etc.
Image.configure({ resizable: true }),
Link.configure({
openOnClick: false,
HTMLAttributes: { class: "text-blue-500 underline" },
}),
Placeholder.configure({
placeholder: "Start writing…",
}),
],
content: "<p>Hello World!</p>",
onUpdate: ({ editor }) => {
console.log(editor.getHTML()) // Or editor.getJSON()
},
})
return (
<div>
{/* Toolbar */}
<div className="toolbar">
<button
onClick={() => editor?.chain().focus().toggleBold().run()}
className={editor?.isActive("bold") ? "is-active" : ""}
>
Bold
</button>
<button
onClick={() => editor?.chain().focus().toggleHeading({ level: 2 }).run()}
>
H2
</button>
</div>
{/* Editor */}
<EditorContent editor={editor} className="editor-content" />
</div>
)
}
Custom extension:
import { Extension, Node } from "@tiptap/core"
// Simple mark extension:
const Highlight = Extension.create({
name: "highlight",
addOptions() {
return { color: "#fef9c3" }
},
addGlobalAttributes() {
return [{
types: ["textStyle"],
attributes: {
color: {
default: null,
parseHTML: (el) => el.style.backgroundColor || null,
renderHTML: (attrs) => attrs.color ? { style: `background-color: ${attrs.color}` } : {},
},
},
}]
},
})
// Node extension (custom block type):
const CalloutNode = Node.create({
name: "callout",
group: "block",
content: "inline*",
parseHTML: () => [{ tag: "div.callout" }],
renderHTML: ({ HTMLAttributes }) => ["div", { class: "callout", ...HTMLAttributes }, 0],
addNodeView() {
return ReactNodeViewRenderer(CalloutComponent) // React component
},
})
Tiptap ecosystem:
- 50+ official extensions (tables, mentions, collaboration, math, etc.)
- Tiptap Cloud — real-time collaborative editing (Hocuspocus/Y.js)
- Pro extensions: AI writing, content browser, export (docx, PDF)
- Used by: Substack, Linear, GitLab, Vercel
Lexical
Lexical was created by Meta as the replacement for Draft.js (deprecated). It powers Facebook comments, Messenger, and internal tools at Meta.
import { LexicalComposer } from "@lexical/react/LexicalComposer"
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"
import { ContentEditable } from "@lexical/react/LexicalContentEditable"
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin"
import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin"
import { HeadingNode, QuoteNode } from "@lexical/rich-text"
import { ListPlugin } from "@lexical/react/LexicalListPlugin"
import { ListNode, ListItemNode } from "@lexical/list"
const editorConfig = {
namespace: "MyEditor",
nodes: [HeadingNode, QuoteNode, ListNode, ListItemNode],
theme: {
heading: { h1: "text-3xl font-bold", h2: "text-2xl font-semibold" },
text: { bold: "font-bold", italic: "italic", underline: "underline" },
},
onError(error: Error) {
console.error(error)
},
}
function LexicalEditor() {
return (
<LexicalComposer initialConfig={editorConfig}>
<div className="editor-wrapper">
<RichTextPlugin
contentEditable={<ContentEditable className="editor-input" />}
placeholder={<div className="placeholder">Start writing…</div>}
ErrorBoundary={LexicalErrorBoundary}
/>
<HistoryPlugin />
<ListPlugin />
<AutoFocusPlugin />
<OnChangePlugin
onChange={(editorState) => {
editorState.read(() => {
const root = $getRoot()
const text = root.getTextContent()
console.log(text)
})
}}
/>
</div>
</LexicalComposer>
)
}
Lexical custom node:
import {
DecoratorNode,
LexicalNode,
NodeKey,
SerializedLexicalNode,
} from "lexical"
export class ImageNode extends DecoratorNode<React.ReactElement> {
__src: string
__altText: string
static getType(): string { return "image" }
static clone(node: ImageNode): ImageNode {
return new ImageNode(node.__src, node.__altText, node.__key)
}
constructor(src: string, altText: string, key?: NodeKey) {
super(key)
this.__src = src
this.__altText = altText
}
exportJSON(): SerializedLexicalNode {
return { type: "image", version: 1, src: this.__src, altText: this.__altText }
}
decorate(): React.ReactElement {
return <img src={this.__src} alt={this.__altText} />
}
createDOM(): HTMLElement {
return document.createElement("div")
}
updateDOM(): false { return false }
}
Lexical's architectural advantages:
- Performance: Minimal reconciliation — updates only changed text ranges, not full re-renders
- React Native: Official support — same editor logic across web and mobile
- Accessibility: ARIA attributes and screen reader announcements built-in
- Immutable state: Editor state is immutable, making undo/redo and history trivial
- Plugin composition: No extension inheritance hierarchy — plugins are independent
Slate.js
Slate.js gives you the most control but makes the fewest decisions. The document model is a custom JSON tree that you define completely:
import { createEditor, Descendant, Editor, Transforms } from "slate"
import { Slate, Editable, withReact, useSlate } from "slate-react"
import { withHistory } from "slate-history"
import { useCallback, useMemo } from "react"
// Define your document schema:
type CustomElement = { type: "paragraph" | "heading"; children: CustomText[] }
type CustomText = { text: string; bold?: boolean; italic?: boolean }
// Extend Slate types:
declare module "slate" {
interface CustomTypes {
Element: CustomElement
Text: CustomText
}
}
const initialValue: Descendant[] = [
{ type: "paragraph", children: [{ text: "Hello World!" }] }
]
function SlateEditor() {
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
const renderElement = useCallback(({ attributes, children, element }) => {
switch (element.type) {
case "heading": return <h2 {...attributes}>{children}</h2>
default: return <p {...attributes}>{children}</p>
}
}, [])
const renderLeaf = useCallback(({ attributes, children, leaf }) => (
<span
{...attributes}
style={{
fontWeight: leaf.bold ? "bold" : "normal",
fontStyle: leaf.italic ? "italic" : "normal",
}}
>
{children}
</span>
), [])
return (
<Slate editor={editor} initialValue={initialValue}>
<Editable
renderElement={renderElement}
renderLeaf={renderLeaf}
onKeyDown={(event) => {
if (event.key === "b" && event.ctrlKey) {
event.preventDefault()
Editor.addMark(editor, "bold", true)
}
}}
/>
</Slate>
)
}
Slate's strengths:
- Completely custom document schema — model your own content types
- No opinions about how your document looks or works
- Good for domain-specific editors (code editors, form builders, JSON editors)
Slate's weaknesses:
- You build everything from scratch — no built-in toolbar, shortcuts, or common blocks
- Collaboration (Y.js) requires significant plumbing
- Breaking changes between versions have been a persistent issue
Quill
Quill is the legacy option. It was the dominant choice from 2015–2020 and still has the highest download count due to installed base:
import Quill from "quill"
// Simple JavaScript API — no React integration:
const quill = new Quill("#editor", {
theme: "snow",
modules: {
toolbar: [["bold", "italic"], [{ header: [1, 2, false] }], ["link", "image"]]
}
})
// For React, use react-quill (but it has React 18 issues):
// npm install react-quill — WARNING: no React 18 SSR support
Why Quill is legacy:
- TypeScript support added after the fact — not first-class
- No React 18 / Server Components compatibility
- The Delta format is non-standard and hard to map to modern document schemas
- Last major release was 2.0 in 2022 — development pace has slowed
- react-quill is abandoned with known React 18 SSR breakage
Feature Comparison
| Feature | Tiptap | Lexical | Slate.js | Quill |
|---|---|---|---|---|
| React integration | ✅ First-class | ✅ First-class | ✅ First-class | ⚠️ Via react-quill |
| TypeScript | ✅ | ✅ | ✅ | ⚠️ Partial |
| React Native | ❌ | ✅ | ❌ | ❌ |
| Extension system | ✅ Excellent | ✅ Plugin-based | ✅ Plugin-based | ⚠️ Limited |
| Collaboration (Y.js) | ✅ Built-in/Cloud | ✅ Community | ✅ Community | ❌ |
| Custom node views | ✅ React component | ✅ Decorator nodes | ✅ | ❌ |
| Toolbar | ✅ Extension | DIY | DIY | ✅ Built-in |
| Table support | ✅ Extension | ✅ Community | ✅ Community | ⚠️ Limited |
| Markdown shortcuts | ✅ Extension | ✅ Plugin | ✅ | ❌ |
| Floating menus | ✅ BubbleMenu | DIY | DIY | ❌ |
| Image upload | ✅ Extension | ✅ Plugin | ✅ | ⚠️ Basic |
| Output format | HTML/JSON | JSON | JSON | Delta |
| Active development | ✅ | ✅ Meta-backed | ⚠️ Slower | ⚠️ Slow |
| Commercial support | ✅ Tiptap Pro | ❌ | ❌ | ❌ |
When to Use Each
Choose Tiptap if:
- You want the fastest path to a production-quality editor
- You need common features (tables, mentions, collaboration) without building them
- Your team doesn't want to learn ProseMirror internals
- The Tiptap Cloud subscription for collaborative editing is acceptable
Choose Lexical if:
- React Native support is required (same editor on web and mobile)
- Performance is critical at large document scale
- You want Meta's production backing for long-term maintenance
- You prefer the plugin/composability model over extension inheritance
Choose Slate.js if:
- You need a completely custom document model (not standard rich text)
- Building a domain-specific editor (structured content, form builder, code notebook)
- You have the development time to build toolbar and features from scratch
Don't use Quill if:
- Starting a new project in 2026
- You're using React 18+ with SSR
Real-World Collaboration: Y.js Integration
Collaborative editing is where the library choice becomes architectural. All three modern editors (Tiptap, Lexical, Slate.js) can integrate with Y.js for real-time collaboration, but the integration depth varies significantly.
Tiptap's collaboration is the most seamless: the @tiptap/extension-collaboration and @tiptap/extension-collaboration-cursor packages integrate directly with Y.js and Tiptap Cloud (Hocuspocus). You add two extensions and you have presence cursors, conflict-free merges, and offline persistence. Tiptap Cloud handles the WebSocket server side, or you can self-host Hocuspocus. The entire setup is under 20 lines of configuration.
Lexical's Y.js integration requires more manual wiring. The @lexical/yjs package provides the binding, but you handle the WebSocket provider and awareness protocol yourself. The upside is control: you can route through your own server, implement custom conflict resolution for domain-specific node types, and integrate with existing WebSocket infrastructure. Meta uses Lexical internally with custom real-time infrastructure, which is why the lower-level API is a feature, not a bug, for teams with similar needs.
Slate.js collaborative editing requires the most implementation work. There is no official Y.js binding — the community maintains slate-yjs, which works but requires understanding Slate's data model deeply to handle conflicts correctly. For teams where collaborative editing is a core feature (not a nice-to-have), this is the most important differentiator. Tiptap's integrated approach saves weeks of implementation time.
Performance at Scale: Large Documents
Editor performance becomes critical when documents exceed ~50,000 words or contain hundreds of embedded media elements. The underlying data structure determines how each editor handles large documents.
Tiptap (ProseMirror) uses a flat node tree with position-based addressing. Every change triggers a full document parse to validate the schema. For most documents, this is imperceptible. For very large documents (novels, long-form research, wikis), ProseMirror's approach can cause visible lag during collaborative editing when many remote changes arrive simultaneously.
Lexical was specifically designed for large documents. Its immutable editor state and minimal reconciliation mean it only re-renders text ranges that actually changed — not the whole document. Meta has validated this at scale in Facebook's comment composer, Instagram captions, and Messenger, all of which handle millions of daily active users. If your use case involves user-generated content at scale, Lexical's rendering model is meaningfully more efficient.
Slate.js performance depends entirely on how you implement rendering. Because Slate hands you full control over renderElement and renderLeaf, you can implement virtualization for long documents. Teams building code editors or structured editors with Slate often implement virtual rendering to handle 10,000+ line documents. This flexibility is Slate's superpower for specialized use cases.
Storage Format and Database Persistence
How editor output maps to your database schema is a practical concern that often gets overlooked during library evaluation.
Tiptap JSON is a clean, versioned document structure that maps well to PostgreSQL JSONB or MongoDB documents. The schema is stable across Tiptap versions (minor changes only), making it safe to store long-term. Tiptap's HTML output is also reliable — useful for email rendering or legacy system integration.
Lexical JSON is also structured and stable, but the format is Lexical-specific. It encodes node types and their properties, which means your stored documents are tied to the specific node types you've defined. If you add a new node type, existing documents don't break — they just don't have the new node's data. Serializing to markdown or HTML requires additional serializers.
Slate JSON is fully custom — you define the schema, so the stored format is exactly what you need for your domain. This is ideal if your documents aren't generic rich text (structured forms, content blocks, schema-validated templates). The tradeoff is that you own the migration path if your schema evolves.
Quill Delta format is the most constrained. Deltas are a sequence of insert/retain/delete operations that represent transformations from an empty document. While compact, deltas don't map naturally to a tree structure, making it harder to query specific content sections from a database. Most teams migrating off Quill cite the Delta format as a primary motivator.
Choosing Based on Team Profile
For product teams building a general-purpose content editor (blog, docs, CMS, newsletter): Tiptap is the right choice. The extension ecosystem covers 95% of use cases, collaboration is first-class, and the React integration is excellent. The cost of Tiptap Pro extensions (AI assist, content browser) is often justified by the development time saved.
For teams building voice-of-the-customer tools, social platforms, or messaging apps where the editor is not the core product: Lexical. It initializes fast, handles concurrent edits efficiently, and doesn't require installing a heavy extension ecosystem for basic rich text. Meta's backing means it will be maintained for the long term.
For teams building specialized document editors — legal contracts, technical specifications, structured data entry, code notebooks — Slate.js is worth the investment. The ability to define an arbitrary document schema means you can model your domain precisely, not approximate it with generic rich text constructs.
For teams maintaining legacy applications that can't afford a full migration: Quill is still functional. Don't break what works. Plan a migration path for the next major feature cycle.
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on official documentation for @tiptap/core v2.x, lexical v0.19.x, slate v0.104.x, and quill v2.0.x. Bundle sizes from bundlephobia for core packages only. Collaboration feature comparison based on official Y.js integration guides and community implementations.
Compare rich text editor packages on PkgPulse →
See also: React vs Vue and React vs Svelte, Best React Form Libraries (2026).