contenteditable Basics
contenteditable surfaces — what the browser gives you, sanitization, keyboard and a11y gaps, and when to pick a real editor library.
- 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 adiv. - Keyboard: users expect shortcuts; don’t steal them without documentation.
- Placeholder patterns need
aria-placeholderor 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
- XSS via
innerHTML. - Assuming identical markup in Chrome vs Safari.
- Broken undo after custom DOM mutations.
- Focus loss when re-rendering React controlled contenteditable.
- 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
inputwithout 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.
Related
Further reading
- contenteditable — MDN
- Input Events Level 2
- Why contenteditable is terrible — historical essays / editor eng blogs
Related guides
- Accessibility Tree OverviewHow browsers build the accessibility tree from DOM and CSS — roles, names, states, what’s pruned, and how to inspect it in DevTools.
- Audio and Video ElementsNative audio/video — controls, sources, captions, autoplay policies, and accessibility requirements for media on the web.
- Autocomplete and Name Attributesname and autocomplete on form fields — password managers, autofill tokens, and why missing names break real users more than demos.
- Base Element and Relative URLsHow <base href> rewrites relative URLs for links, scripts, and forms — powerful for static hosts, dangerous when set accidentally.
- data Attributesdata-* attributes for embedding element metadata — dataset API, CSS hooks, validation limits, and when not to store app state in the DOM.