ESC

Type to search the knowledge base.

contentEditable Pitfalls

Why contentEditable is hard: browser HTML mess, carets, sanitization, undo, and when to pick a real editor framework.

advanced3 min read
  • javascript
  • contenteditable
  • dom
  • editors

contentEditable="true" turns any element into a mini word processor. Browsers disagree on the HTML they insert, paste is a security and consistency nightmare, and caret control is a specialized skill. Treat it as a sharp tool — fine for tiny inline renames, painful for a Google Docs clone without a framework.

The basic switch

<div id="editor" contenteditable="true" role="textbox" aria-multiline="true"></div>
const editor = document.querySelector('#editor');

editor.addEventListener('input', () => {
  // fires on text changes (not all browsers historically equal)
  saveDraft(editor.innerHTML);
});

Prefer innerText/textContent when you only need plain text. Prefer a structured model (JSON doc) when you need rich text long-term — HTML-as-source-of-truth ages badly.

Browser HTML is not stable

Press Enter in a contenteditable and you might get <div>, <p>, or <br> depending on browser and parent. Bold might be <b> or <strong> or styles.

// execCommand is deprecated but still widely used in legacy code
document.execCommand('bold');
document.execCommand('insertHTML', false, '<span class="mention">@ada</span>');

document.execCommand is deprecated; new work should use InputEvent APIs, beforeinput, and/or an editor library (ProseMirror, Lexical, Slate, TipTap).

Paste: sanitize or lose

editor.addEventListener('paste', (e) => {
  e.preventDefault();
  const text = e.clipboardData.getData('text/plain');
  // insert plain text only — simplest safe default
  document.execCommand('insertText', false, text);
});

Pasting from Docs/Word injects styles, classes, and sometimes scripts in older edge cases. Never assign innerHTML from clipboard HTML without a strict sanitizer (and even then, prefer a structured paste pipeline).

Selection and caret

function placeCaretAtEnd(el) {
  el.focus();
  const range = document.createRange();
  range.selectNodeContents(el);
  range.collapse(false);
  const sel = window.getSelection();
  sel.removeAllRanges();
  sel.addRange(range);
}

React-controlled contenteditable is a classic footgun: re-rendering resets the caret. Either go uncontrolled for the editable surface or restore selection after each render (hard).

Security

// XSS if another user can set this HTML
editor.innerHTML = userProvidedHtml;

If content is stored and shown to others, treat it like any HTML sink: sanitize server-side and client-side, or store a non-HTML document format.

When contentEditable is OK

Use case Verdict
Single-line rename, plain text OK with contenteditable + paste plain
Comment box with @mentions Prefer library or careful model
Full rich text / collaborative Use a real editor
Markdown source Use <textarea>
// plain-text editable with fewer surprises
editor.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') {
    e.preventDefault();
    editor.blur();
  }
});

Interview answer

“contentEditable makes an element editable but browsers emit inconsistent HTML, paste is unsafe without sanitization, and caret management fights virtual DOM frameworks. I use it only for small plain-text surfaces, sanitize or force plain paste, and pick a dedicated editor for rich text. execCommand is deprecated legacy.”

beforeinput and inputType

Modern editors listen to beforeinput to cancel or reinterpret edits before the DOM mutates:

editor.addEventListener('beforeinput', (e) => {
  // e.inputType: insertText, deleteContentBackward, formatBold, ...
  if (e.inputType === 'insertFromPaste') {
    e.preventDefault();
    const text = e.dataTransfer?.getData('text/plain') ?? '';
    insertPlain(text);
  }
});

Support varies; always feature-detect and have a paste fallback. For collaborative editing, contentEditable HTML is a terrible CRDT source — use a document model (ProseMirror/Lexical schema) and treat the DOM as a projection. That architecture is why “just contentEditable” rarely survives contact with real product requirements.

Further reading

Related guides