The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
Create a rich-text host<div contenteditable="true" aria-label="Post body"> </div>View examples
Create a plain-text host<div contenteditable="plaintext-only" aria-label="Summary"> </div>View examples
Opt out inside a host<span contenteditable="false">Locked token</span>View examples
Inspect effective stateeditor.isContentEditableView examples
Observe editing intenteditor.addEventListener('beforeinput', handleBeforeInput)View examples
Read operation typeevent.inputType === 'insertText'View examples
React after mutationeditor.addEventListener('input', saveDraft)View examples
Read plain textconst value = editor.textContentView examples
Set trusted plain texteditor.replaceChildren(document.createTextNode(value))View examples
Focus a nested host<div contenteditable="true" tabindex="0"></div>View examples
Suggest a keyboard<div contenteditable="plaintext-only" inputmode="numeric"> </div>View examples
Request spellchecking<div contenteditable="true" spellcheck="true"></div>View examples
Render saved text safelypreview.textContent = savedValueView examples

The contenteditable attribute turns an element into an editing host, but it does not provide a document model, toolbar, sanitizer, history policy, or reliable serialization format. Treat browser editing as an input surface: choose rich or plain-text behavior deliberately, observe editing intent before mutation, preserve keyboard and focus conventions, and validate every persisted result at a trusted boundary.

Step by step

Detailed examples

01

Choose rich text, plain text, and inheritance deliberately

contenteditable is enumerated rather than Boolean: use the strings true, false, or plaintext-only. Missing or invalid values inherit from the parent, and HTMLElement.isContentEditable reports the effective result. Rich editing permits user-agent-generated markup; plaintext-only requests text editing and removes formatting when content is pasted, but support and exact editing behavior should still be tested in target browsers.

A rich body with an immutable token and plain summary
<label id="body-label">Post body</label>
<div id="body" contenteditable="true" aria-labelledby="body-label">
  Draft <span contenteditable="false">#1042</span>
</div>

<label id="summary-label">Plain-text summary</label>
<div id="summary" contenteditable="plaintext-only" aria-labelledby="summary-label"></div>

<script>
  console.log(body.isContentEditable); // true
</script>
Back to quick reference ↑
02

Separate cancellable intent from completed mutation

beforeinput describes the proposed edit through inputType and, for some operations, data or dataTransfer. Cancel only operations your editor model can replace correctly; composition, spellchecking, autofill, and some platform edits may be non-cancelable. The later input event signals that the DOM changed. Keyboard events alone are insufficient because editing can originate from paste, drag, speech, handwriting, or assistive technology.

Limit plain-text input without depending on keydown
<div id="headline" contenteditable="plaintext-only" aria-label="Headline"></div>
<p id="count"></p>
<script>
  const limit = 80;
  headline.addEventListener('beforeinput', (event) => {
    if (!event.cancelable || !event.inputType.startsWith('insert')) return;
    const incoming = event.data ?? '';
    if (headline.textContent.length + incoming.length > limit) event.preventDefault();
  });
  headline.addEventListener('input', () => {
    count.textContent = `${headline.textContent.length}/${limit}`;
  });
</script>
Back to quick reference ↑
03

Make the application data model explicit

The DOM inside a rich editing host is browser-managed presentation, not a stable cross-browser interchange format. Use textContent when the product stores text. For structured editors, translate permitted DOM shapes into a defined model and render that model back through DOM construction. Setting text with a Text node avoids HTML parsing and preserves literal characters such as angle brackets.

Save and restore a plain-text draft
<div id="note" contenteditable="plaintext-only" aria-label="Note"></div>
<button id="restore" type="button">Restore</button>
<script>
  let draft = '';
  note.addEventListener('input', () => { draft = note.textContent; });
  restore.addEventListener('click', () => {
    note.replaceChildren(document.createTextNode(draft));
    note.focus();
  });
</script>
Back to quick reference ↑
04

Preserve focus, composition, and virtual-keyboard behavior

An editing host participates in focus navigation, but nested hosts are not automatically added to the tab sequence; add tabindex=0 only when the nested editor is independently operable. inputmode is a keyboard hint, not validation. Avoid intercepting ordinary editing shortcuts, and do not save partial input during an active IME composition unless the product explicitly supports that workflow.

Defer validation until composition ends
<div id="amount" contenteditable="plaintext-only" inputmode="decimal" aria-label="Amount"></div>
<p id="message" role="status"></p>
<script>
  let composing = false;
  amount.addEventListener('compositionstart', () => { composing = true; });
  amount.addEventListener('compositionend', validate);
  amount.addEventListener('input', () => { if (!composing) validate(); });
  function validate(event) {
    composing = false;
    message.textContent = /^\d+(?:[.,]\d+)?$/.test(amount.textContent) ? '' : 'Enter a number.';
  }
</script>
Back to quick reference ↑
05

Treat editable HTML as untrusted input

Users can introduce markup through paste, drag and drop, DOM tools, restored drafts, and synchronization—not only through typing. Never trust content because it originated in a contenteditable element. Prefer plain text; if rich content is required, enforce an allowlist with a maintained sanitizer at ingestion and again on the server, normalize URLs, and apply a restrictive Content Security Policy as defense in depth.

Create a preview without executing edited markup
<div id="editor" contenteditable="plaintext-only" aria-label="Message">Hello</div>
<output id="preview"></output>
<script>
  editor.addEventListener('input', () => {
    const savedValue = editor.textContent;
    preview.textContent = savedValue;
  });
</script>
Back to quick reference ↑

Local code tester

Inspect editing intent and safe text output

Edit the note to see beforeinput operations and a text-only preview without treating user content as markup.

Runs in your browser
Preview

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. WHATWGHTML Standard: User interactionhtml.spec.whatwg.org
  2. World Wide Web ConsortiumInput Events Level 2w3.org
  3. WHATWGDocument Object Model Standarddom.spec.whatwg.org
  4. World Wide Web ConsortiumSelection APIw3.org

Help us improve

Found a typo or missing example?

Tell us what would make this cheat sheet clearer, more complete, or more useful.

Share feedback