# Rich Text Editor > Own a document model; never trust raw contentEditable output. Frontend architecture guidance from fearchitect, written by Abas Turabli and last reviewed 2026-06-21. Source: https://fearchitect.com/topics/rich-text-editor Use it as reference for the task at hand. Before changing code, check this guidance against the codebase: where the code already makes a different, deliberate choice, flag the conflict instead of rewriting it. Library APIs move faster than this guide, so confirm exact signatures in the official docs linked at the end. ## 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. ## 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 ``, Firefox may insert ``, 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. ## Watch out: 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 | Concern | Lexical (Meta) | ProseMirror / TipTap | Slate | | --- | --- | --- | --- | | **Bundle size** | ~22 kB min+gz core | ProseMirror ~50 kB; TipTap adds ~20 kB on top | ~100 kB with React peer | | **Document model** | Typed node classes; immutable state snapshots | Schema-validated node/mark graph; strict by design | Plain JSON AST; flexible but less constrained | | **React integration** | First-class; ships React binding from Meta | TipTap wraps ProseMirror with React/Vue adapter | Built for React; renders via virtual DOM | | **Collaborative editing** | Yjs binding via y-lexical (community) | Yjs via y-prosemirror (official); mature | Community Yjs plugin; less battle-tested | | **Schema strictness** | Enforced via node registration; prevents invalid trees | ProseMirror schema is strict; TipTap extensions add nodes | Ad-hoc; schema is your responsibility | | **Maturity** | Stable since 2022; powers Facebook composer | ProseMirror since 2015; production-grade | Stable 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. ## Decision: 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. ## 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. ## Related topics - [Real-time: WebSockets vs SSE vs Polling](https://fearchitect.com/topics/realtime-websockets-sse-polling.md): Match the right real-time transport to your data-flow direction. - [XSS, CSRF & Clickjacking](https://fearchitect.com/topics/xss-csrf-clickjacking.md): Three browser-level attacks and the headers that stop them. - [CSP & Trusted Types](https://fearchitect.com/topics/csp-trusted-types.md): Block script injection and DOM XSS at the browser's policy layer. - [State Machines](https://fearchitect.com/topics/state-machines.md): Model UI as explicit states with typed transitions to kill impossible states. - [Autocomplete / Typeahead](https://fearchitect.com/topics/autocomplete-typeahead.md): Debounced input, AbortController cancellation, and ARIA combobox. - [Data Table / Data Grid](https://fearchitect.com/topics/data-table.md): Headless table logic separate from rendering, with server-side ops for large datasets. ## Further reading - [Lexical — official docs](https://lexical.dev/docs/intro) - [ProseMirror guide](https://prosemirror.net/docs/guide/) - [TipTap — getting started](https://tiptap.dev/docs/editor/getting-started/overview) - [Yjs — shared editing docs](https://docs.yjs.dev/) - [y-prosemirror — GitHub](https://github.com/yjs/y-prosemirror)