contenteditable

contenteditable — Rich Text Editor Demos

5 interactive examples

The #1 contenteditable bug — and the fix

Select text in each editor, then click Bold. Left: selection is lost (nothing happens). Right: onmousedown="event.preventDefault()" keeps the selection.

❌ Broken — selection lost on button click
Select some of this text, then click Bold above.
✅ Fixed — onmousedown prevents focus loss
Select some of this text, then click Bold above.
--:--:--Select text in either editor and click a toolbar button

The complete fix — save and restore pattern

// 1. Save range when editor loses focus let savedRange = null; editor.addEventListener('blur', () => { const sel = window.getSelection(); if (sel.rangeCount > 0) savedRange = sel.getRangeAt(0).cloneRange(); }); // 2. Restore before applying command function applyFormat(cmd) { editor.focus(); if (savedRange) { const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(savedRange); savedRange = null; } document.execCommand(cmd); } // 3. onmousedown prevents focus steal (simpler fix) <button onmousedown="event.preventDefault()" onclick="applyFormat('bold')">Bold</button>

CSS-only placeholder — no JavaScript needed

❌ No placeholder (raw contenteditable)

User has no idea what to type here

✅ CSS :empty::before placeholder

Placeholder disappears as soon as user types
/* CSS-only — no JS required */ [contenteditable][data-placeholder]:empty::before { content: attr(data-placeholder); color: #9ca3af; pointer-events: none; cursor: text; }
--:--:--Click the right editor — type to dismiss placeholder

All contenteditable values

contenteditable="true"
Rich text editing — bold, links, paste formatting.
contenteditable="plaintext-only"
Plain text only — no bold, no links. Try pasting rich content.
contenteditable="false" (inside editable parent)
Editable text — read-only island — editable again.
Not editable
No contenteditable attribute — just a div.

Complete rich text editor — toolbar active states, keyboard shortcuts, auto-save

Select text and use toolbar buttons or keyboard shortcuts (Ctrl+B, Ctrl+I, Ctrl+U). Active buttons update as you move the cursor. Content auto-saves to localStorage after 1.5s.

0 characters Draft
--:--:--Type and format text — toolbar highlights active states · Ctrl+B/I/U work

Paste sanitization — strip formatting from clipboard

Copy some styled text from a website or document, then paste into both editors. Left keeps all HTML. Right strips to plain text.

❌ Raw paste (no interception)

innerHTML after paste:

✅ Sanitized paste (plain text only)

innerHTML after paste:
// Intercept paste event, strip formatting editor.addEventListener('paste', (e) => { e.preventDefault(); const text = e.clipboardData.getData('text/plain'); document.execCommand('insertText', false, text); });
--:--:--Paste into either editor — see the innerHTML difference

Modern Selection API — wrap selected text without execCommand

Select text in the editor, then use the buttons below. These use window.getSelection() and Range.surroundContents() — no execCommand needed.

Select some of this text and use the buttons above. The modern Selection API wraps it in elements directly — no execCommand required. Try highlighting multiple phrases or inserting a @mention at the cursor.
Click "Inspect selection" after selecting text
// Wrap selected text in a custom element function wrapSelection(tagName) { const sel = window.getSelection(); if (!sel.rangeCount || sel.isCollapsed) return; const range = sel.getRangeAt(0); const wrapper = document.createElement(tagName); try { range.surroundContents(wrapper); } catch { // Crosses element boundaries — use extractContents fallback wrapper.appendChild(range.extractContents()); range.insertNode(wrapper); } }
--:--:--Select text then click a button — Selection API wraps it
Read the tutorial