ESC

Type to search the knowledge base.

contenteditable Basics

contenteditable surfaces — what the browser gives you, sanitization, keyboard and a11y gaps, and when to pick a real editor library.

advanced3 min read
  • html
  • contenteditable

contenteditable makes an element’s content user-editable. It’s the foundation of many rich-text editors — and a minefield of browser inconsistencies, paste sanitation, and accessibility work.

Docs: MDN contenteditable, execCommand legacy, Input Events.

Basic usage

<div
  class="editor"
  contenteditable="true"
  role="textbox"
  aria-multiline="true"
  aria-label="Release notes"
></div>
.editor {
  min-height: 8rem;
  padding: 0.75rem;
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  white-space: pre-wrap;
}

Values: true / empty string (editable), false (not), plaintext-only (where supported — no rich formatting).

What you get for free

  • Caret, selection, basic typing
  • Some browser undo stacks
  • IME composition for international input (usually)

What you don’t get:

  • Consistent HTML output across browsers
  • Safe paste from Word/Google Docs
  • Collaborative editing
  • Perfect screen reader announcements for rich formatting
  • A stable document model (you get a live DOM)

Reading and writing content

const el = document.querySelector(".editor");

// Prefer input events over deprecated execCommand for new work
el.addEventListener("input", () => {
  const html = el.innerHTML;
  const text = el.innerText;
  // persist / validate
});

document.execCommand is obsolete; modern editors use beforeinput / input and their own models (or libraries like ProseMirror, Lexical, TipTap, Slate).

Sanitization is mandatory

// NEVER assign unsanitized user HTML to the live DOM of other users
el.innerHTML = userHtmlFromServer; // XSS vector if not sanitized

Treat editor HTML as untrusted. Sanitize on the server with a vetted library; CSP as defense in depth. Prefer a constrained schema (bold/italic/links only).

Accessibility gaps

  • Announce the control as a text field (role="textbox", aria-multiline="true") when using a div.
  • Keyboard: users expect shortcuts; don’t steal them without documentation.
  • Placeholder patterns need aria-placeholder or visible labels — CSS-only placeholders often fail AT.
  • Toolbar buttons for bold/italic must be real buttons with pressed states (aria-pressed).
<div class="toolbar" role="toolbar" aria-label="Formatting">
  <button type="button" aria-pressed="false" data-cmd="bold">Bold</button>
</div>

When to use a library

Need Approach
One-line rename contenteditable or <input>
Comments with light markdown Consider textarea + markdown preview
Docs / email composer Editor framework
Nested tables, comments, collab Specialized editor

Interview out-loud

“contenteditable makes elements editable but doesn’t provide a clean document model or safe paste. I label it as a multiline textbox for a11y, listen to input events, sanitize HTML aggressively, and reach for an editor framework when rich text is a product feature—not a weekend execCommand demo.”

Footguns

  1. XSS via innerHTML.
  2. Assuming identical markup in Chrome vs Safari.
  3. Broken undo after custom DOM mutations.
  4. Focus loss when re-rendering React controlled contenteditable.
  5. Using contenteditable for form fields that should be inputs (autofill, validation).

React controlled pitfall

If React re-renders and sets innerHTML from state on every keystroke, the caret jumps to the start. Patterns that work:

  • Uncontrolled contenteditable with refs + periodic state sync
  • Libraries that map a document model to DOM carefully
  • Avoid putting contenteditable values in React state each input without selection restoration
// Sketch: read on blur for simple fields
el.addEventListener("blur", () => onChange(el.innerText));

For plain text only, a <textarea> is almost always better than contenteditable.

Further reading

Related guides