Skip to main content

Tiptap vs Lexical vs Slate.js vs Quill: Rich Text Editors in React (2026)

·PkgPulse Team

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

PackageWeekly DownloadsBackingBundle Size
quill~1.3MCommunity~400KB
@tiptap/core~1.2MOpen-source + enterprise~56KB
@lexical/react~1.1MMeta~65KB
slate~550KCommunity~45KB

Architecture Overview

LibraryFoundationPhilosophy
TiptapProseMirrorOpinionated, extensions, React wrapper
LexicalFrom scratchLean core, plugins, framework-agnostic
Slate.jsFrom scratchPlugin-first, maximum control
QuillDelta formatSimple 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:

  1. Performance: Minimal reconciliation — updates only changed text ranges, not full re-renders
  2. React Native: Official support — same editor logic across web and mobile
  3. Accessibility: ARIA attributes and screen reader announcements built-in
  4. Immutable state: Editor state is immutable, making undo/redo and history trivial
  5. 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

FeatureTiptapLexicalSlate.jsQuill
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✅ ExtensionDIYDIY✅ Built-in
Table support✅ Extension✅ Community✅ Community⚠️ Limited
Markdown shortcuts✅ Extension✅ Plugin
Floating menus✅ BubbleMenuDIYDIY
Image upload✅ Extension✅ Plugin⚠️ Basic
Output formatHTML/JSONJSONJSONDelta
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

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.

Compare rich text editor packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.