The contenteditable Attribute
The contenteditable attribute turns any element into
something the user can click into and edit directly, right in the
rendered page — no <textarea> required. It is the
foundation many simple rich-text editors are built on.
Basic Usage
contenteditable-basic.html
<div contenteditable="true"> This text can be clicked and edited in place. </div>
contenteditable="true"(or justcontenteditable) makes the element editable.contenteditable="false"explicitly disables editing (useful to override an inherited editable state).Without the attribute at all, most elements default to non-editable.
Inheritance
contenteditable is inherited by children unless they
explicitly override it, which lets you make one part of a nested UI
editable while carving out non-editable "islands" inside it.
contenteditable-inheritance.html
<div contenteditable="true">
<p>You can edit this paragraph.</p>
<p contenteditable="false">
This one is locked, even though its parent is editable.
</p>
</div>Use Case: A Simple Rich-Text Editor
Combined with a toolbar and the older{' '}
document.execCommand API (or, in modern code, direct DOM
manipulation / the newer{' '}
Selection and Range APIs), a{' '}
contenteditable region becomes a lightweight editor.
mini-editor.html
<div id="toolbar">
<button data-cmd="bold"><b>B</b></button>
<button data-cmd="italic"><i>I</i></button>
</div>
<div id="editor" contenteditable="true">
Start typing your note here...
</div>
<script>
document.getElementById('toolbar').addEventListener('click', (event) => {
const cmd = event.target.dataset.cmd
if (cmd) {
document.execCommand(cmd) // deprecated — see note below
}
})
</script>execCommand Is Deprecated
document.execCommand is marked obsolete in the HTML spec and behaves inconsistently across browsers. It still works today for quick demos, but production rich-text editors should use the modern Selection/Range APIs directly, or — far more commonly in practice — a maintained editor library (e.g. TipTap, Lexical, ProseMirror, Quill) built on those primitives.Listening for Changes
The input event fires on a contenteditable{' '}
element whenever its content changes, just like it does on form fields —
useful for autosaving or live character counts.
contenteditable-input-event.js
const editor = document.getElementById('editor')
editor.addEventListener('input', () => {
console.log('Current content:', editor.innerHTML)
})Current content: Start typing your note here... Current content: Start typing your note here! Edited.
Security: Sanitize Before Saving
editor.innerHTML can contain anything the user typed, pasted, or dragged in — including <script> tags, event-handler attributes, or malicious links. If you save this HTML and later render it for other users (or even the same user), you must sanitize it on the server (or with a trusted client-side sanitizer like DOMPurify) before it is ever rendered again.Never store raw
innerHTMLfrom acontenteditableregion and re-render it unsanitized elsewhere.Prefer a strict allow-list sanitizer over trying to blacklist "bad" tags yourself.
Sanitize again on the server even if you also sanitize on the client — client-side checks can be bypassed.
Accessibility Considerations
A
contenteditableregion should usually haverole="textbox"and, if multi-line,aria-multiline="true"for screen reader users.Provide a visible focus style — editable regions are easy to miss without one.
For most simple forms, a real
<textarea>is more accessible and far less code than a customcontenteditableeditor — reach forcontenteditablewhen you specifically need rich formatting.
contenteditable involves surprisingly gnarly edge cases (undo/redo, paste handling, cursor position across browsers). For anything beyond a toy example, a maintained editor framework will save significant time.Quick Reference
Attribute value | Effect |
|---|---|
| Element becomes editable |
| Element is explicitly not editable |
(attribute absent) | Not editable, and does not inherit editability |