Rich Text Editor Design: What to Look For
Rich text editor design, explained simply. See the schema and storage choices that hold up over time in a real CMS.

A rich text editor is not a text box with bold and italic buttons. Underneath the toolbar is a document model. It's a set of rules for what content can contain what, plus a serialization format for turning that model into something a database can store. Get the schema wrong and every editor built on top inherits the problem.
This is the design layer, not the library layer. Whether you build on ProseMirror, Lexical, or Slate, or skip rich-text JSON for MDX entirely: the same four questions decide whether the result holds up in production.
What Is a Rich Text Editor's Document Model?
Every serious editor treats the DOM as a rendering target, not a source of truth. contenteditable renders pixels; it doesn't hold state. The real state lives in a document tree. It's a nested structure of nodes and marks that the editor keeps in memory and re-renders on every change.
ProseMirror makes this explicit with a strict schema. You declare exactly what node types exist and what each one may contain. The editor rejects any edit that would produce an invalid tree (ProseMirror docs). Slate takes the opposite stance and assumes almost nothing about your schema, trading validation for flexibility. Lexical, built by Meta, sits between the two with a node-based model tuned for React and TypeScript.
Why the schema choice outlasts the library choice
Migrating from one editor library to another is disruptive. It's survivable, though, as long as the document model stays similar. Migrating from a loose schema to a strict one, or back, usually means touching every stored document. Pick the schema's strictness deliberately, before picking a library to implement it.
How Do Edits Actually Happen?
Every serious editor routes changes through transactions, not direct DOM mutation. A transaction describes one edit, insert text, toggle a mark, split a node, and gets applied to the document tree as a discrete, reversible step.
// A transaction describes the edit, not the DOM change
editor.dispatch(
editor.state.tr.insertText('Draft saved', selectionEnd)
);
This is also where undo comes from for free. Since every change is a transaction, undo just replays the inverse transaction instead of tracking raw DOM diffs, which is fragile and slow at scale.
Serialization: Where the Real Cost Shows Up
A document tree in memory is only useful once it can be saved and rendered somewhere else. That's serialization, and it's the step most schema decisions get judged on months later, not at design time.
Rich-text-as-JSON formats, ProseMirror's document JSON, Lexical's serialized state, Portable Text, store the tree as a nested object. Rendering that tree means writing a serializer function per node type: one for headings, one for code blocks, one for embeds. Every one has to stay in sync as the schema grows.
MDX skips that layer. The stored format is already Markdown plus JSX, so rendering means compiling the string, not walking a custom tree and dispatching to per-node renderers. The cost moves from render time to compile time, and the renderer code shrinks because there's no node-type switch statement to maintain.
What breaks in each direction
A rich-text-JSON schema that adds a node type after launch needs a serializer update everywhere it renders: web, email, PDF export. Only then is the new node type safe to use. An MDX schema that adds a new component needs the component registered in the renderer's components map, one place. It gains a different risk: MDX compiles to executable JS. An unregistered or malicious component reference fails loudly instead of silently rendering blank.
Selection and Cursor State
A document model isn't complete without a selection model. Selections in a real editor aren't just a start and end DOM offset. They're anchored to positions in the document tree itself. A selection stays valid across re-renders even as surrounding content changes.
This matters more than it sounds. A naive DOM-offset selection breaks the moment collaborative editing or an async save reflows the document underneath the cursor. A tree-anchored selection survives it.
What Should a Content Schema Actually Support?
Four things separate a schema that scales from one that gets rewritten in a year.
- Nested marks without ambiguity. Bold-inside-a-link-inside-italic needs a defined precedence, not implicit DOM nesting order.
- Custom nodes with typed data. A callout or embed block needs its own shape, not a generic "block" with a loosely typed payload.
- A defined block-vs-inline boundary. Deciding upfront which nodes can nest inside a paragraph avoids invalid states no validator catches later.
- A stable serialization target. Whatever the storage format, JSON tree or MDX string, changing it after launch is a migration, not a config change.
When Is a Loose Schema the Right Call?
A strict schema isn't always the right answer. Slate's minimal-assumptions design exists because some editors genuinely don't know their content shape upfront, an internal tool, a prototype, or a plugin system where third parties define their own node types.
A loose schema trades safety for speed of iteration. You skip weeks of schema design and start shipping. The cost lands later: without a validator, a malformed document can slip into storage and only surface as a broken render, in production, for one specific user.
Match the choice to how many people write content, and how much you trust them. A single internal team publishing through a reviewed workflow can tolerate a loose schema. A public-facing CMS with dozens of editors across different skill levels cannot; a bad paste from Word or Google Docs will find every gap the schema didn't close.
There's a middle path most teams miss. Validate at the boundary instead of inside the editor: let the editor stay loose for iteration speed, but run a strict check when content saves, not when it renders. That catches the same bad state without slowing down every keystroke with full-tree validation.
The Underused Angle
Most rich text editor guides compare libraries: ProseMirror against Lexical against Slate. They skip the schema question, as if any library can bolt onto any content strategy. It can't. A strict-schema editor like ProseMirror paired with a loosely typed storage layer just moves the validation gap. It shifts from the editor to the API, where it's harder to catch.
The real design decision isn't which library renders the toolbar. It's whether your schema enforces its rules at write time, in the editor. Or does it discover violations at read time, in whatever renders the content months later? Fixing a bad write-time schema costs an afternoon. Fixing a read-time surprise in production costs a migration.
Conclusion
A rich text editor's document model, transaction pipeline, and serialization format decide how a schema ages, not the library badge on the toolbar. Draftbase sidesteps the serializer-per-node-type cost. Content stores as MDX strings in typed richText fields, and @draftbase/renderer compiles them straight to React instead of walking a custom tree. Content schema keeps breaking every time you add a block type? Draftbase's content modeling tools catch that at write time instead of render time.
Ship content that's built to be found
Draftbase generates schema, structured data, and a fast MDX editor for every post.
Frequently asked questions
What is a rich text editor's data model?
It's the data structure behind the toolbar. A tree of nodes and marks the editor keeps in memory. The DOM only renders it; it never holds the real state.
Should a content schema be strict or loose?
Strict for public-facing CMS content with many writers. A validator catches bad pastes before they save. Loose is fine for internal tools with one trusted writer.
Why does MDX need fewer render functions than rich-text JSON?
Rich-text JSON needs one render function per node type. MDX compiles a plain string straight to React. There's no per-node switch statement to maintain.
Is ProseMirror or Lexical better for a strict schema?
ProseMirror. Its schema checks every edit. It rejects anything that would build a broken tree. Lexical allows more freedom by default.
What breaks when a rich text schema has no checks?
A malformed document can save without error. It only fails later, when something tries to render it. That failure often shows up live, for one user, not in testing.