Skip to content

The contenteditable Attribute

Global Attribute

The contenteditable attribute makes an element’s content editable by the user. It’s the foundation for building rich text editors, inline editing interfaces, and interactive content areas directly in the browser.

<element contenteditable="value">Editable content</element>
ValueDescription
true or "" (empty)Content is editable
falseContent is not editable (default)
plaintext-onlyContent is editable, but rich formatting is stripped
Result
Result
Result
Result
Result

contenteditable is inherited by child elements but can be overridden:

<div contenteditable="true">
<p>This is editable</p>
<p contenteditable="false">This is NOT editable</p>
<p>This is editable again</p>
</div>

Editable elements can receive focus like form inputs:

const editor = document.querySelector('[contenteditable]');
editor.focus();
// Get current cursor position
const selection = window.getSelection();
const range = selection.getRangeAt(0);

When content is editable:

  • Enter key: Creates new paragraphs or <div> elements (browser-dependent)
  • Formatting: Browsers insert <b>, <i>, <u> tags when using shortcuts
  • Paste: Formatted content is preserved (unless plaintext-only)
  • Undo/Redo: Browser provides basic undo functionality

Make it obvious when content is editable:

[contenteditable="true"] {
border: 2px dashed #cbd5e1;
padding: 10px;
min-height: 100px;
}
[contenteditable="true"]:hover {
background: #f8fafc;
cursor: text;
}
[contenteditable="true"]:focus {
outline: none;
border-color: #3b82f6;
background: white;
}

For simple text inputs without formatting:

<!-- User comments, plain text fields -->
<div contenteditable="plaintext-only">
Enter your comment...
</div>

Always sanitize content before saving or displaying:

function sanitizeContent(html) {
const temp = document.createElement('div');
temp.textContent = html; // Strips all HTML tags
return temp.innerHTML;
}
// Or use a library like DOMPurify
const clean = DOMPurify.sanitize(dirtyHTML);

Improve user experience with custom keyboard handling:

editor.addEventListener('keydown', (e) => {
// Ctrl+S to save
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
saveContent();
}
// Limit Enter key to line breaks instead of new paragraphs
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
document.execCommand('insertLineBreak');
}
});

When updating content programmatically, preserve the cursor:

function saveSelection() {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
return selection.getRangeAt(0);
}
return null;
}
function restoreSelection(range) {
if (range) {
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
}
<div
contenteditable="true"
aria-label="Document editor"
role="textbox"
aria-multiline="true">
Content...
</div>
<div id="editor" contenteditable="true" style="min-height: 300px; border: 1px solid #ccc; padding: 15px;">
<h1>Document Title</h1>
<p>Start typing...</p>
</div>
<h1 contenteditable="true" class="editable-title">
Click to edit title
</h1>
<pre contenteditable="true" spellcheck="false"><code>function hello() {
console.log('Hello, World!');
}</code></pre>
<div
contenteditable="plaintext-only"
placeholder="Write a comment..."
style="min-height: 100px;">
</div>

Make editable content accessible:

<div
contenteditable="true"
role="textbox"
aria-label="Message content"
aria-multiline="true"
aria-required="true">
</div>

Ensure all functionality is keyboard-accessible:

editor.addEventListener('keydown', (e) => {
// Tab should move to next element, not insert tab character
if (e.key === 'Tab') {
e.preventDefault();
// Move focus to next element
}
});
<!-- Announce changes to screen readers -->
<div
contenteditable="true"
aria-live="polite"
aria-atomic="true">
</div>
const editor = document.getElementById('editor');
// Get content
const html = editor.innerHTML;
const text = editor.textContent;
// Set content
editor.innerHTML = '<p>New content</p>';
editor.textContent = 'Plain text content';
// Format selected text
document.execCommand('bold');
document.execCommand('italic');
document.execCommand('underline');
// Insert content
document.execCommand('insertHTML', false, '<strong>Bold text</strong>');
document.execCommand('insertText', false, 'Plain text');
// Create lists
document.execCommand('insertUnorderedList');
document.execCommand('insertOrderedList');
const selection = window.getSelection();
const selectedText = selection.toString();
// Get cursor position
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const cursorPosition = range.startOffset;
}
editor.addEventListener('input', (e) => {
console.log('Content changed:', e.inputType);
// Save to localStorage, trigger auto-save, etc.
});
editor.addEventListener('beforeinput', (e) => {
// Can prevent input
if (shouldPreventInput(e)) {
e.preventDefault();
}
});

Excellent support across all modern browsers:

FeatureChromeFirefoxSafariEdge
true / falseAllAllAllAll
plaintext-only58+No10.1+79+
  • Selection API - Work with text selection and cursor position
  • execCommand - Format and manipulate editable content (deprecated but still used)
  • Input Events - Detect and respond to content changes