Overview
When you have a contenteditable="true" element, you can set contenteditable="false" on child elements to create read-only sections within an editable area. However, this behavior is inconsistent across browsers and can cause unexpected issues. This document focuses on the problems that occur when contenteditable="false" is used within a contenteditable="true" region.
⚠️ Inconsistent Behavior
contenteditable="false" has inconsistent behavior:
- Some browsers allow editing within
contenteditable="false"elements - Inheritance behavior varies across browsers
- Selection behavior differs when spanning across editable/non-editable boundaries
- Event handling may be inconsistent
Inheritance Behavior
How contenteditable Inheritance Works
The contenteditable attribute is inherited by child elements:
- contenteditable="true": Element and all children are editable (unless overridden)
- contenteditable="false": Element and all children are not editable
- contenteditable="inherit": Inherits from parent (default behavior)
- No attribute: Inherits from parent (same as "inherit")
<!-- Example: Inheritance -->
<div contenteditable="true">
<p>Editable paragraph</p>
<p contenteditable="false">Non-editable paragraph</p>
<p contenteditable="inherit">Inherits editable (editable)</p>
<p>No attribute (inherits editable)</p>
<div contenteditable="true">
<p>Nested editable (editable)</p>
</div>
</div>⚠️ Inheritance Inconsistencies
Problem: When a parent element has contenteditable="true" and a child element has contenteditable="false", the inheritance behavior is inconsistent across browsers. Some browsers allow editing in the child, while others correctly prevent it.
<!-- ❌ BAD: Behavior may be inconsistent -->
<div contenteditable="true">
<p>Editable</p>
<p contenteditable="false">Should not be editable, but may be editable in Chrome</p>
</div>
<!-- ✅ GOOD: Test in all browsers and provide workarounds -->
<div contenteditable="true" id="editor">
<p>Editable</p>
<p contenteditable="false" class="readonly">Non-editable</p>
</div>
<script>
// Workaround: Prevent editing programmatically
const editor = document.getElementById('editor');
const readonlyElements = editor.querySelectorAll('.readonly');
readonlyElements.forEach(el => {
el.addEventListener('beforeinput', (e) => {
e.preventDefault();
e.stopPropagation();
});
el.addEventListener('input', (e) => {
e.preventDefault();
e.stopPropagation();
});
});
</script>Browser Differences
Chrome/Edge
⚠️ contenteditable="false" Not Always Respected
Chrome/Edge: Child elements with contenteditable="false" may still be editable. The attribute is not consistently respected.
<!-- Chrome may allow editing here -->
<div contenteditable="true">
<p contenteditable="false">May still be editable in Chrome!</p>
</div>Firefox
⚠️ Inheritance Inconsistencies
Firefox: Inheritance behavior is inconsistent. Children with contenteditable="false" may still be editable, and children with contenteditable="inherit" may not inherit correctly.
Safari
Safari: Generally better support for contenteditable="false", but still test thoroughly.
Browser Support Summary
| Browser | contenteditable="false" Support | Notes |
|---|---|---|
| Chrome/Edge | ⚠️ Partial | May allow editing in false elements |
| Firefox | ⚠️ Partial | Inheritance inconsistencies |
| Safari | ✅ Better | Generally more reliable |
Selection Behavior
⚠️ Node Selection Not Possible
Problem: Within contenteditable="false" elements, you cannot select entire nodes (like images, videos, or other block elements) using Range.selectNode() or by clicking. The browser treats these elements as non-selectable.
<!-- ❌ BAD: Cannot select image node in contenteditable="false" -->
<div contenteditable="true">
<p>Editable text</p>
<div contenteditable="false">
<img src="image.jpg" alt="Image">
<!-- Cannot select this image node! -->
</div>
</div>
<script>
// This will fail or behave unexpectedly
const img = document.querySelector('img');
const range = document.createRange();
range.selectNode(img); // May not work if img is in contenteditable="false"
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range); // Selection may be lost or invalid
</script>
<!-- ✅ GOOD: Move image to editable area or handle programmatically -->
<div contenteditable="true">
<p>Editable text</p>
<img src="image.jpg" alt="Image" contenteditable="false">
<!-- Image can be selected here -->
</div>Impact: This makes it impossible to programmatically select or manipulate nodes within non-editable sections, even if you only want to select them (not edit them).
⚠️ Text Input After Selection
Problem: When a selection includes or is within a contenteditable="false" element, typing text may not work as expected. The browser may:
- Ignore the input entirely
- Move the cursor to an unexpected location
- Insert text in the wrong place
- Clear the selection without inserting text
<!-- ❌ BAD: Selection includes non-editable element -->
<div contenteditable="true">
<p>Editable text</p>
<div contenteditable="false">
<p>Non-editable text</p>
</div>
<p>More editable text</p>
</div>
<!-- User selects from "Editable text" through "Non-editable text" to "More editable text" -->
<!-- Then types "new text" -->
<!-- Result: Text may not be inserted, or inserted in wrong location! -->
<!-- ✅ GOOD: Normalize selection before input -->
element.addEventListener('beforeinput', (e) => {
const selection = window.getSelection();
if (selection.rangeCount === 0) return;
const range = selection.getRangeAt(0);
// Check if selection includes non-editable content
const nonEditableInRange = range.commonAncestorContainer.querySelectorAll(
'[contenteditable="false"]'
);
if (nonEditableInRange.length > 0) {
// Normalize selection to exclude non-editable content
e.preventDefault();
normalizeSelectionToEditable(range);
// Then handle input manually
handleInputManually(e.data);
}
});Selection Across Editable/Non-Editable Boundaries
When selection spans across editable and non-editable elements, behavior can be inconsistent:
- Selection may include non-editable content
- Deleting selection may remove non-editable elements
- Pasting may insert content into non-editable areas
- Formatting may be applied to non-editable content
- Text input after selection may fail or behave unexpectedly
- Node selection (images, videos) is not possible within contenteditable="false"
// Example: Normalize selection to exclude non-editable content
function normalizeSelection() {
const selection = window.getSelection();
if (selection.rangeCount === 0) return;
const range = selection.getRangeAt(0);
// Check if selection includes non-editable elements
const nonEditableElements = range.commonAncestorContainer.querySelectorAll(
'[contenteditable="false"]'
);
if (nonEditableElements.length > 0) {
// Adjust selection to exclude non-editable content
const startContainer = range.startContainer;
const endContainer = range.endContainer;
// Find nearest editable node (text node or element)
const editableStart = findEditableNode(startContainer);
const editableEnd = findEditableNode(endContainer);
if (editableStart && editableEnd) {
// Set selection to editable boundaries
if (editableStart.nodeType === Node.TEXT_NODE) {
range.setStart(editableStart, 0);
} else {
range.setStartBefore(editableStart);
}
if (editableEnd.nodeType === Node.TEXT_NODE) {
range.setEnd(editableEnd, editableEnd.textContent.length);
} else {
range.setEndAfter(editableEnd);
}
selection.removeAllRanges();
selection.addRange(range);
}
}
}
function findEditableNode(node) {
// If node is in non-editable area, find nearest editable ancestor
let current = node;
while (current && current !== document.body) {
if (current.contentEditable === 'true') {
// Found editable ancestor, return first text node or the element itself
if (current.nodeType === Node.TEXT_NODE) {
return current;
}
// Return first text node in editable area
const walker = document.createTreeWalker(
current,
NodeFilter.SHOW_TEXT,
null
);
return walker.nextNode() || current;
}
if (current.contentEditable === 'false') {
// We're in a non-editable area, need to go up
current = current.parentNode;
continue;
}
current = current.parentNode;
}
return null;
}
// Prevent text input when selection includes non-editable content
element.addEventListener('beforeinput', (e) => {
const selection = window.getSelection();
if (selection.rangeCount === 0) return;
const range = selection.getRangeAt(0);
const nonEditableInRange = range.commonAncestorContainer.querySelectorAll(
'[contenteditable="false"]'
);
if (nonEditableInRange.length > 0 && e.inputType === 'insertText') {
// Normalize selection first
normalizeSelection();
// Then allow the input to proceed
// Or prevent and handle manually
}
});Use Cases
Common Use Cases for contenteditable=false
- Embedded Media: Prevent editing of images, videos, or other embedded content
- Read-only Sections: Create read-only sections within an editable document
- Template Elements: Protect template or boilerplate content from editing
- Metadata: Keep metadata or annotations non-editable
- Complex Widgets: Prevent editing within complex interactive widgets
<!-- Example: Embedded media -->
<div contenteditable="true">
<p>Editable text before image</p>
<img src="image.jpg" contenteditable="false" alt="Non-editable image">
<p>Editable text after image</p>
</div>
<!-- Example: Read-only sections -->
<div contenteditable="true">
<p>Editable paragraph</p>
<div contenteditable="false" class="readonly-section">
<h2>Read-only Header</h2>
<p>This section cannot be edited</p>
</div>
<p>Editable paragraph</p>
</div>
<!-- Example: Template elements -->
<div contenteditable="true">
<p contenteditable="false" class="template">
[Template: Insert your content here]
</p>
<p>Your editable content</p>
</div>Workarounds
1. Prevent Events Programmatically
Intercept and prevent editing events on non-editable elements:
function makeNonEditable(element) {
// Prevent all input events
element.addEventListener('beforeinput', (e) => {
e.preventDefault();
e.stopPropagation();
return false;
}, true); // Use capture phase
element.addEventListener('input', (e) => {
e.preventDefault();
e.stopPropagation();
return false;
}, true);
// Prevent keyboard input
element.addEventListener('keydown', (e) => {
// Allow navigation keys
const allowedKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight',
'Home', 'End', 'PageUp', 'PageDown'];
if (!allowedKeys.includes(e.key) && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
e.stopPropagation();
return false;
}
}, true);
// Prevent paste
element.addEventListener('paste', (e) => {
e.preventDefault();
e.stopPropagation();
return false;
}, true);
}
// Apply to all non-editable elements
document.querySelectorAll('[contenteditable="false"]').forEach(makeNonEditable);2. Monitor and Revert Changes
Monitor DOM changes and revert any edits to non-editable elements:
function protectNonEditable(element) {
const nonEditableElements = element.querySelectorAll('[contenteditable="false"]');
nonEditableElements.forEach(el => {
const originalHTML = el.innerHTML;
// Monitor changes
const observer = new MutationObserver((mutations) => {
if (el.innerHTML !== originalHTML) {
// Revert changes
el.innerHTML = originalHTML;
}
});
observer.observe(el, {
childList: true,
subtree: true,
characterData: true
});
});
}
protectNonEditable(document.getElementById('editor'));3. Use CSS to Indicate Non-Editable
Use CSS to visually indicate non-editable elements and prevent cursor changes:
/* CSS for non-editable elements */
[contenteditable="false"] {
user-select: none;
cursor: default;
background-color: rgba(0, 0, 0, 0.05);
pointer-events: none; /* Prevents interaction */
}
/* Or allow selection but not editing */
[contenteditable="false"].selectable {
user-select: text;
cursor: text;
pointer-events: auto;
}Platform-Specific Issues
Browser-Specific Issues
⚠️ Chrome: Editing Allowed
Chrome: May allow editing within contenteditable="false" elements. Use programmatic event prevention as workaround.
⚠️ Firefox: Inheritance Issues
Firefox: Inheritance behavior is inconsistent. Test thoroughly and provide workarounds.
ℹ️ Safari: Better Support
Safari: Generally better support, but still test edge cases.
Mobile-Specific Issues
⚠️ Mobile: Touch Selection
Mobile devices: Touch selection may behave differently with non-editable elements. Test on actual devices.