Overview
The insertParagraph inputType is triggered when the user presses Enter to create a new paragraph. The DOM structure created varies significantly between browsers.
Basic Behavior
When Enter is pressed in a contenteditable element, the browser splits the current paragraph and creates a new one. However, the exact DOM structure depends on the browser and the current selection state.
Scenario 1: Cursor in middle of paragraph
Before (Cursor at position 5)
After Enter (Chrome/Edge)
After Enter (Firefox)
After Enter (Safari)
Edge Cases
Scenario 2: Cursor at end of paragraph
Before
After Enter (Most browsers)
Scenario 3: Cursor at start of paragraph
Before
After Enter (Chrome/Edge)
Scenario 4: Text selected across paragraphs
Before (Selection spans multiple paragraphs)
After Enter (Replaces selection)
Browser-Specific Differences
DOM Element Used for Paragraphs
Chrome/Edge
- Uses
<p>elements - Empty paragraphs:
<p></p> - May add
<br>in some cases
Firefox
- Uses
<p>elements - Empty paragraphs:
<p><br></p> - Always includes
<br>in empty paragraphs
Safari
- May use
<div>instead of<p> - Behavior can vary by macOS version
- Empty paragraphs:
<div><br></div>or<div></div>
Important: These differences mean that the same insertParagraph operation can produce different DOM structures across browsers. Your code must handle all variations.
IME Composition + insertParagraph
⚠️ Critical Issue
When IME composition is active and the user presses Enter, the behavior becomes unpredictable:
beforeinputwithinsertParagraphmay fire beforecompositionend- Composition may be cancelled, losing partial input
- The new paragraph may contain incomplete composition text
See: IME & Composition for more details.
Editor Internal Model & DOM Synchronization
⚠️ Exception: preventDefault() and Block Splitting
When editors use preventDefault() and split blocks in their model:
- Browser's default paragraph insertion is prevented
- Editor splits block in its internal model, then re-renders DOM
- Browser may not recognize the new paragraph structure if DOM is completely re-rendered
- Selection position may need manual restoration after DOM update
- Browser's undo stack may not include the paragraph split if default was prevented
Best Practice: After splitting blocks and re-rendering, ensure the cursor is positioned correctly in the new paragraph block.
Editor-Specific Handling
Handling insertParagraph
Slate normalizes paragraph structure to ensure consistency:
import { Editor, Transforms } from 'slate';
element.addEventListener('beforeinput', (e) => {
if (e.inputType === 'insertParagraph') {
e.preventDefault();
// Slate handles paragraph insertion through Transforms
Transforms.splitNodes(editor, {
always: true, // Always split, even at edges
});
// Ensure new node is a paragraph
Transforms.setNodes(editor, { type: 'paragraph' });
}
});
- Normalization: Always creates consistent paragraph structure regardless of browser.
- Transforms.splitNodes: Splits the current block and creates a new one.
- Type enforcement: Ensures new blocks are paragraphs, not divs or other elements.
Handling insertParagraph
ProseMirror uses commands to handle paragraph insertion:
import { splitBlock } from 'prosemirror-commands';
// In keymap
{
Enter: (state, dispatch) => {
// ProseMirror's splitBlock command handles insertParagraph
return splitBlock(state, dispatch);
}
}
// Or intercept beforeinput
view.dom.addEventListener('beforeinput', (e) => {
if (e.inputType === 'insertParagraph') {
e.preventDefault();
const { state, dispatch } = view;
if (splitBlock(state, dispatch)) {
// Handled
}
}
});
- Command-based: Uses
splitBlockcommand. - Schema enforcement: Schema defines what block types are allowed.
- Consistent structure: Always produces schema-compliant DOM.
Handling insertParagraph
Draft.js handles paragraph insertion through block splitting:
import { EditorState, RichUtils } from 'draft-js';
function handleReturn(e, editorState) {
// Draft.js splits ContentBlocks on Enter
const newState = RichUtils.insertSoftNewline(editorState);
// Or use insertData to insert paragraph break
// Normalize to ensure consistent block structure
return newState;
}
// In key binding
function myKeyBindingFn(e) {
if (e.keyCode === 13) { // Enter
return 'insert-paragraph';
}
return getDefaultKeyBinding(e);
}
- Block-based: Splits ContentBlocks, not DOM elements.
- RichUtils: Provides utilities for block manipulation.
- Serialization: Converts blocks to consistent HTML on render.