Design and Implementation Considerations for Building a Rich Text Editor from Scratch
This article explores the motivations, architectural choices, zero‑width character handling, data‑structure design, and implementation strategies—including execCommand, ContentEditable, and Canvas approaches—for creating a custom rich‑text editor that balances flexibility, performance, and browser compatibility.
Rich text editors allow users to format and structure content with a WYSIWYG experience, but most existing solutions focus on the application layer, leaving the underlying architecture under‑explored. The author argues that building an editor from the ground up is valuable for understanding low‑code concepts, DSL‑based DOM manipulation, and for addressing subtle issues that are often hidden in mature editors.
Key motivations include the desire to experiment with core data structures, improve zero‑width character handling for cursor placement and visual effects, and to design a linear delta model inspired by Quill that simplifies operations compared to the more complex nested models used by Slate or ProseMirror.
Zero‑width characters (e.g., ​ or U+200B ) are essential for maintaining line height, enabling block‑level selection, and ensuring cursor placement in inline‑block or contenteditable‑false nodes. Example markup demonstrates how inserting a zero‑width character at the end of a line creates the expected visual selection effect:
<span data-string="true" data-enter="true" data-leaf="true">\u200B</span>The article compares three architectural levels for editors:
L0 : Simple ContentEditable with document.execCommand , minimal customization.
L1 : ContentEditable with a custom data model and command layer, used by many open‑source editors.
L2 : Canvas‑based rendering with a self‑implemented layout engine, offering full control but higher complexity.
For L1 implementations, the author outlines how to abstract the model, selection, and rendering loop, showing a minimal example:
const editor = { selection: {}, execCommand: (cmd, val) => { /* update model and render */ } };
const model = [{ type: "bold", text: "123" }, { type: "span", text: "123123" }];
const render = () => { /* render DOM from model */ };When using execCommand , browser inconsistencies (e.g., different handling of <div><br></div> vs. <br> on Enter) are highlighted, emphasizing the need for a custom model to achieve consistent behavior.
The article also discusses the challenges of integrating ContentEditable with modern frameworks like React, where warnings about uncontrolled children arise, and the trade‑offs of hiding an <input> element to capture IME input while rendering the document via custom DOM or Canvas.
Finally, the author plans to implement a ContentEditable‑based editor with a linear delta data structure, leveraging the discussed concepts to build modules for input handling, clipboard operations, and selection management, acknowledging that such a project will be a substantial engineering effort.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.