Understanding contenteditable and Modern Rich Text Editors with Slate.js

The article explains how the HTML contenteditable attribute enables in‑place editing, why legacy execCommand‑based editors are inconsistent, and how modern frameworks such as Slate.js—built on React and Immutable.js—provide a structured, location‑based data model with Transform and Node APIs for reliable, portable rich‑text editing.

Bilibili Tech
Bilibili Tech
Bilibili Tech
Understanding contenteditable and Modern Rich Text Editors with Slate.js

The contenteditable attribute is an enumerated HTML attribute that indicates whether an element can be edited by the user. When set to true or an empty string, the element becomes editable; when set to false, it is not editable. If the attribute is omitted, its value is inherited from the parent element.

Example of making any element behave like a textarea:

<div contenteditable="true"><br/>  This text can be edited by the user.<br/></div>

Compatibility tables show that most modern browsers support

contenteditable</strong>.</p><p>Traditional rich‑text editors often rely on <code>document.execCommand

to apply formatting, insert elements, or handle copy‑paste. Example:

function formatDoc(sCmd, sValue) {<br/>  if (validateMode()) { document.execCommand(sCmd, false, sValue); oDoc.focus(); }<br/>}

Note: document.execCommand is deprecated and may be removed from future browsers.

Pain points of traditional editor solutions

Inconsistent behavior across browsers (e.g., handling of the Enter key).

Verbose and error‑prone DOM manipulation, especially after the rise of MVVM frameworks like Vue and React.

Difficulty persisting content: storing raw innerHTML leads to challenges with content moderation, algorithmic analysis, layout changes, and multi‑platform rendering.

Modern editors such as Quill.js and Draft.js address these issues by providing a structured, platform‑agnostic data model.

Slate.js Overview

Slate.js is a maintained, contenteditable -based editor framework that draws inspiration from Quill and Draft. It operates as a framework rather than a ready‑made application, built on top of React and Immutable.js.

Core APIs

Transform – document operations Transforms.setNodes(editor, { type: 'heading-one' }) To revert a heading back to plain text:

Transforms.unwrapNodes(editor, {<br/>  match: node => !Editor.isEditor(node) && node.children?.every(child => Editor.isBlock(editor, child)),<br/>  mode: 'all',<br/>})

Inserting text at a specific location:

Transforms.insertText(editor, 'some words', {<br/>  at: { path: [0, 0], offset: 3 },<br/>})

Node – element types

interface Editor {<br/>  // Current editor state<br/>  children: Node[]<br/>  selection: Range | null<br/>  operations: Operation[]<br/>  marks: Omit<Text, 'text'> | null<br/>  isInline: (element: Element) => boolean<br/>  isVoid: (element: Element) => boolean<br/>  normalizeNode: (entry: NodeEntry) => void<br/>  onChange: () => void<br/>  addMark: (key: string, value: any) => void<br/>  apply: (operation: Operation) => void<br/>  deleteBackward: (unit: 'character' | 'word' | 'line' | 'block') => void<br/>  deleteForward: (unit: 'character' | 'word' | 'line' | 'block') => void<br/>  deleteFragment: () => void<br/>  insertBreak: () => void<br/>  insertSoftBreak: () => void<br/>  insertFragment: (fragment: Node[]) => void<br/>  insertNode: (node: Node) => void<br/>  insertText: (text: string) => void<br/>  removeMark: (key: string) => void<br/>}

Elements (called Element) can be block or inline; void elements are treated as black boxes. Leaf nodes are Text objects that store the actual string and formatting flags.

Locations – addressing nodes

Three main location types are used:

Path : an array of indices that uniquely identifies a node in the tree. Example:

const editor = {<br/>  children: [<br/>    {<br/>      type: 'paragraph',<br/>      children: [<br/>        { text: 'A line of text!' }<br/>      ]<br/>    }<br/>  ]<br/>}

The leaf Text node has the path [0, 0]. The editor root has the special path [].

Point : a Path plus an offset indicating the cursor position.

interface Point {<br/>  path: Path;<br/>  offset: number;<br/>}

Range : two points defining a selection.

interface Range {<br/>  anchor: Point;<br/>  focus: Point;<br/>}

Descendant – the editor’s data structure

The editor’s content can be retrieved as an array of Descendant objects (essentially Node or Text items). This structure can be stringified and stored on a server.

const initialValue: Descendant[] = [<br/>  {<br/>    type: 'paragraph',<br/>    children: [<br/>      { text: 'This is editable ' },<br/>      { text: 'rich', bold: true },<br/>      { text: ' text, ' },<br/>      { text: 'much', italic: true },<br/>      { text: ' better than a ', },<br/>      { text: '<textarea>', code: true },<br/>      { text: '!' }<br/>    ]<br/>  },<br/>  {<br/>    type: 'paragraph',<br/>    children: [<br/>      { text: "Since it's rich text, you can do things like turn a selection of text " },<br/>      { text: 'bold', bold: true },<br/>      { text: ', or add a semantically rendered block quote in the middle of the page, like this:' }<br/>    ]<br/>  },<br/>  { type: 'block-quote', children: [{ text: 'A wise quote.' }] },<br/>  { type: 'paragraph', align: 'center', children: [{ text: 'Try it out for yourself!' }] }<br/>];

The article concludes with a preview of building a modern rich‑text editor using Slate.js and provides reference links to MDN, Quill, Draft, and related resources.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

frontendJavaScriptReactrich-text-editorcontenteditableSlate.js
Bilibili Tech
Written by

Bilibili Tech

Provides introductions and tutorials on Bilibili-related technologies.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.