Phenomenon
A regression or long-standing divergence in Firefox (reported active in Lexical Playground, Nov 2025) prevents the default “drag-to-move” behavior within contenteditable. While Chromium and WebKit allow users to reposition text blocks intuitively via drag-and-drop, Firefox often fails to dispatch the necessary drop events or internal DOM updates required to teleport the text.
Reproduction Steps
- Open a
contenteditableeditor in Firefox (v130+). - Type two sentences.
- Select the first sentence with the mouse.
- Click and hold the selection, then drag it to the end of the second sentence.
- Release the mouse button.
Observed Behavior
dragstart: Fires correctly.- Ghost Image: Appears and follows the mouse.
drop: Either does not fire at the target, or fires but the browser’s default action (moving the text) is not executed.- Result: The selection remains at the source, and nothing is moved. No
beforeinputwithinputType: "deleteByDrag"or"insertFromDrop"is triggered.
Expected Behavior
The browser should automatically handle the deletion of the source fragment and the insertion at the destination, triggering beforeinput events for both operations.
Impact
- Severed UX: Users who rely on mouse-based editing (common in elderly users or specific workflows) find the editor “broken.”
- Framework Incompatibility: Modern frameworks (Lexical, Slate) expect the browser to manage the basic move operation or provide a valid
DataTransferobject during the drop.
Browser Comparison
- Firefox 130-132: Reported failure in move operations.
- Chrome / Edge: Works natively and smoothly.
- Safari: Works correctly on macOS.
References & Solutions
Mitigation: Manual Drag-Drop Handler
If the browser fails to move the text, you must implement a complete drag-and-drop manager using the DataTransfer API.
element.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', window.getSelection().toString());
e.dataTransfer.effectAllowed = 'move';
});
element.addEventListener('drop', (e) => {
e.preventDefault();
const data = e.dataTransfer.getData('text/plain');
const range = document.caretRangeFromPoint(e.clientX, e.clientY);
// Manually delete source and insert at range
// NOTE: This usually requires a complex transaction logic in frameworks
dispatchMoveTransaction(sourceRange, range, data);
});