Skip to content
fearchitect
UI System-Design Patterns

Rich Text Editor

Own a document model; never trust raw contentEditable output.

By Abas TurabliReviewed

Summary

Raw `contentEditable` lets the browser own the DOM, producing cross-browser inconsistencies and dirty HTML that is hard to sanitize or serialize. Production editors — ProseMirror, Lexical, TipTap, Slate — own a typed document model and render from it, making serialization, validation, and collaborative editing tractable. Choose the framework by bundle size, schema flexibility, and collaborative-editing needs.

Jump to the interview angle

Document-model editor

A rich text editor built on contentEditable hands the browser control of the DOM. Every browser vendor implements editing commands differently — Chrome inserts <b>, Firefox may insert <strong>, and Safari diverges further. The result is arbitrary, inconsistent HTML that is unsafe to store or render without heavy sanitization.

Document-model editors (ProseMirror, Lexical, TipTap, Slate) invert this: they maintain a typed in-memory tree (nodes, marks, attributes), derive contentEditable DOM purely for display, and intercept every keystroke and selection event. The model is the source of truth; the DOM is a view. Serialization becomes a deterministic traversal of the model, not a parse of dirty HTML.

Never store or render raw contentEditable HTML

Any HTML collected from a contentEditable element is XSS-dangerous and browser-dependent. If you must accept raw HTML (e.g., paste from Word), run it through a server-side sanitizer (DOMPurify on the server, or a dedicated library like sanitize-html) with a strict allowlist. The safe default is to store the editor's own JSON model and derive HTML at render time from your own serializer.

Framework comparison: Lexical, ProseMirror/TipTap, Slate

ConcernLexical (Meta)ProseMirror / TipTapSlate
Bundle size~22 kB min+gz coreProseMirror ~50 kB; TipTap adds ~20 kB on top~100 kB with React peer
Document modelTyped node classes; immutable state snapshotsSchema-validated node/mark graph; strict by designPlain JSON AST; flexible but less constrained
React integrationFirst-class; ships React binding from MetaTipTap wraps ProseMirror with React/Vue adapterBuilt for React; renders via virtual DOM
Collaborative editingYjs binding via y-lexical (community)Yjs via y-prosemirror (official); matureCommunity Yjs plugin; less battle-tested
Schema strictnessEnforced via node registration; prevents invalid treesProseMirror schema is strict; TipTap extensions add nodesAd-hoc; schema is your responsibility
MaturityStable since 2022; powers Facebook composerProseMirror since 2015; production-gradeStable since 2016; large ecosystem

Selection, serialization, and collaborative editing

  • Use the editor's Selection API, not `window.getSelection()` — models normalize cross-browser range quirks.
  • Serialize to your own schema (JSON or Markdown), never to innerHTML; derive HTML from the model at read time.
  • Collaborative editing via Yjs uses CRDTs: each client applies operations locally, and Yjs merges divergent histories without a central lock.
  • y-prosemirror maps ProseMirror Steps to Yjs operations; both peers converge to the same document after any network partition.
  • Presence (cursors, selections of other users) requires a separate awareness layer — Yjs provides `y-protocols/awareness` for this.

Which framework to pick

Choose Lexical for a React-first project where bundle size matters and you want first-party Meta support. Choose TipTap (over raw ProseMirror) when you need a batteries-included extension library and proven Yjs collaboration. Choose Slate when you need deep control over the data model and already have a custom JSON schema. Avoid raw contentEditable in all cases.

Interview angle

Interviewers probe whether you reach for contentEditable directly or know why a document model is mandatory. Show you can compare frameworks and explain serialization safety. Cover CRDTs for the collaborative variant.

Soundbite: "The editor owns the model; the DOM is just a view of it."

Key terms

document model
Typed in-memory tree the editor owns; DOM is derived from it, not the reverse.
ProseMirror
Low-level editor toolkit with a strict schema-validated node/mark graph; foundation of TipTap.
Lexical
Meta's extensible editor framework; typed node classes, immutable state, small core bundle.
CRDT (Conflict-free Replicated Data Type)
Data structure that merges concurrent edits from multiple clients without coordination.
Yjs
CRDT library for shared data types; y-prosemirror and y-lexical adapt it to editor operations.

Further reading

Search fearchitect

Jump to a topic, mode, or action.