insertParagraph

How insertParagraph inputType creates new paragraphs and varies across browsers.

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)

HTML:
<p>Hello|world</p>
Visual: Hello|world

After Enter (Chrome/Edge)

HTML:
<p>Hello</p>
<p>|</p>
<p>world</p>
Creates empty paragraph between

After Enter (Firefox)

HTML:
<p>Hello</p>
<p>|<br></p>
<p>world</p>
Adds <br> in empty paragraph

After Enter (Safari)

HTML:
<p>Hello</p>
<div>|</div>
<p>world</p>
Uses <div> instead of <p>

Edge Cases

Scenario 2: Cursor at end of paragraph

Before

<p>Hello world|</p>

After Enter (Most browsers)

<p>Hello world</p>
<p>|</p>
Creates new empty paragraph

Scenario 3: Cursor at start of paragraph

Before

<p>|Hello world</p>

After Enter (Chrome/Edge)

<p></p>
<p>|Hello world</p>
Creates empty paragraph before

Scenario 4: Text selected across paragraphs

Before (Selection spans multiple paragraphs)

<p>Paragraph 1</p>
<p>Paragraph 2</p>

After Enter (Replaces selection)

<p>Para</p>
<p>|</p>
<p>graph 2</p>
Selection deleted, new paragraph inserted

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:

  • beforeinput with insertParagraph may fire before compositionend
  • 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

Slate.js

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.
ProseMirror

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 splitBlock command.
  • Schema enforcement: Schema defines what block types are allowed.
  • Consistent structure: Always produces schema-compliant DOM.
Draft.js

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.

Related resources