Input Types & Formatting Events

Understanding inputType values, their behavior, and edge cases in contenteditable elements.

Overview

The inputType property in beforeinput and input events indicates what kind of operation is being performed. However, the behavior can vary significantly based on:

  • Whether IME composition is active
  • Whether the selection is collapsed or has text selected
  • The browser and operating system
  • Whether the event is beforeinput or input

Editor Internal Models & DOM Synchronization

⚠️ Critical Exception: preventDefault() and Internal Models

When editors use internal models and preventDefault():

  • inputType events may fire, but editors intercept them with preventDefault()
  • Editors update their internal model, then re-render DOM programmatically
  • Browser's undo stack may not be updated (because default behavior was prevented)
  • Browser and editor undo stacks become out of sync
  • Programmatic DOM updates (especially in Safari) may clear the browser's undo stack
  • Selection may become invalid after DOM re-render, requiring manual restoration

Impact: Events like historyUndo may not fire when expected, or may fire but be ignored by the editor. Undo/redo operations may not work correctly if browser and editor stacks are out of sync.

Common Scenarios

1. preventDefault() on beforeinput

  • Editor prevents default, updates model, re-renders DOM
  • Browser's undo stack not updated
  • Editor maintains separate undo stack

2. Programmatic DOM Updates

  • Using innerHTML, textContent, or React re-renders
  • May clear browser's undo stack (especially Safari)
  • Browser doesn't recognize programmatic changes as user actions

3. Model-to-DOM Re-rendering

  • After undo in editor's model, DOM is re-rendered
  • Browser sees DOM change but doesn't know it's from undo
  • historyUndo event may not fire

Best Practices

  • Always preventDefault() on historyUndo/historyRedo: Intercept these events and use your own undo stack
  • Maintain separate undo stack: Don't rely on browser's undo stack when using internal models
  • Sync selection after DOM updates: Always restore selection after model changes and DOM re-renders
  • Avoid programmatic DOM updates during undo: If possible, use browser's undo stack for simple cases, or fully manage your own
  • Use document.execCommand() carefully: May trigger browser undo, which can conflict with internal undo

Critical Edge Cases

⚠️ macOS + Korean IME + Collapsed Cursor + Formatting Shortcuts

macOS specific critical issue: During Korean IME composition with a collapsed cursor, pressing formatting shortcuts (e.g., Cmd + B, Cmd + I, Cmd + U, Cmd + K) behaves unexpectedly:

  • Formatting events (e.g., formatBold) do not fire
  • Instead, insertCompositionText event fires again
  • Formatting is completely ignored
  • The composition text may be inserted/committed instead

Impact: Formatting shortcuts are completely non-functional during Korean IME composition on macOS, making it impossible to apply formatting.

⚠️ General IME Composition + Formatting Shortcuts

Problem: During IME composition (e.g., typing Japanese, Korean, Chinese), pressing formatting shortcuts like Cmd/Ctrl + B may cause beforeinput to not fire, and only input fires. This means you cannot prevent the default behavior.

Impact: You lose the ability to intercept and customize formatting during composition, and the browser's default formatting is applied.

// Problem: During IME composition, beforeinput may not fire
element.addEventListener('beforeinput', (e) => {
  if (e.inputType === 'formatBold') {
    e.preventDefault(); // This may not fire during composition!
    applyBold();
  }
});

// Solution: Check composition state
element.addEventListener('beforeinput', (e) => {
  if (e.isComposing) {
    // IME composition is active
    // beforeinput may not fire for formatBold
    return;
  }
  
  if (e.inputType === 'formatBold') {
    e.preventDefault();
    applyBold();
  }
});

⚠️ Collapsed Selection + Formatting Shortcuts

Problem: When the selection is collapsed (cursor position, no text selected), pressing formatting shortcuts like Cmd/Ctrl + B may result in:

  • Only beforeinput fires (no input event)
  • Neither event fires (browser handles silently)
  • Browser applies "bold state" to next typed character (toggle behavior)

Impact: You cannot reliably detect or prevent formatting when the cursor is collapsed, making it difficult to implement custom formatting logic.

// Problem: Collapsed selection + Cmd+B may not fire input event
const selection = window.getSelection();
if (selection.isCollapsed) {
  // Cursor position, no text selected
  // Cmd+B may not trigger input event
  // Only beforeinput might fire, or neither
}

// Solution: Handle both events and check selection state
element.addEventListener('beforeinput', (e) => {
  if (e.inputType === 'formatBold') {
    const selection = window.getSelection();
    if (selection.isCollapsed) {
      // Apply bold to next typed character
      // or toggle bold state for current position
      e.preventDefault();
      toggleBoldState();
    } else {
      e.preventDefault();
      applyBoldToSelection();
    }
  }
});

element.addEventListener('input', (e) => {
  // May not fire for collapsed selection + formatBold
  // Always check selection state
  const selection = window.getSelection();
  if (!selection.isCollapsed && e.inputType === 'formatBold') {
    // Handle formatting after the fact
  }
});

Event Firing Patterns

When beforeinput vs input Fires

Normal Case (Non-composition, Non-collapsed)

  1. beforeinput fires with inputType
  2. You can call e.preventDefault()
  3. input fires after DOM is updated (if not prevented)

IME Composition Active

  1. beforeinput may not fire for formatting shortcuts
  2. input fires directly (cannot prevent)
  3. Browser applies default formatting

Collapsed Selection

  1. beforeinput may or may not fire (browser-dependent)
  2. input typically does not fire
  3. Browser may toggle "formatting state" for next character

Collapsed + IME Composition

  1. Most unpredictable case
  2. Events may fire in unexpected order or not at all
  3. Browser behavior varies significantly

Mobile Text Prediction (Android Samsung Keyboard)

  1. beforeinput may not fire before suggestion insertion
  2. Multiple input events may fire for a single suggestion
  3. Suggested text inserted as bulk (full phrase) rather than character-by-character
  4. Event timing differs from manual typing

Browser-Specific Behavior

Known Browser Differences

Chrome/Edge

  • During IME composition: beforeinput may not fire for formatBold
  • Collapsed selection: input does not fire, only beforeinput
  • Toggles formatting state for next typed character

Firefox

  • More consistent event firing during composition
  • Collapsed selection: May fire both events or neither
  • Behavior can vary by OS

Safari

  • Different event patterns, especially on macOS
  • IME composition handling differs from Chrome
  • May use different inputType values

Chrome on Android (Samsung Keyboard)

  • Text prediction/suggestion: beforeinput may not fire before suggestion insertion
  • Suggested text inserted as bulk, causing multiple input events
  • Event timing differs from manual typing
  • May replace more content than expected when selecting suggestions

Editor-Specific Handling

Different editors handle these edge cases differently. Here's how they approach IME composition and collapsed selection:

Slate.js

Handling Composition + Formatting

Slate checks composition state before applying formatting:

    import { Editor, Transforms } from 'slate';

function handleFormatBold(editor: Editor, e: InputEvent) {
  // Check if composition is active
  if (e.isComposing) {
    // During IME composition, formatBold may not work as expected
    // Store intent and apply after composition ends
    return;
  }
  
  const isActive = isBoldActive(editor);
  Transforms.setNodes(
    editor,
    { bold: !isActive },
    { match: n => Text.isText(n), split: true }
  );
}

element.addEventListener('beforeinput', (e) => {
  if (e.inputType === 'formatBold') {
    e.preventDefault();
    handleFormatBold(editor, e);
  }
});
  
  • Composition detection: Checks e.isComposing before handling formatBold.
  • Deferred formatting: Can store formatting intent and apply after composition ends.
  • State management: Maintains formatting state separately from DOM.
ProseMirror

Handling Composition + Formatting

ProseMirror uses keymap and composition state:

    import { toggleMark } from 'prosemirror-commands';

// ProseMirror handles composition in keymap
const keymap = {
  'Mod-b': (state, dispatch, view) => {
    // Check if IME composition is active
    if (view.composing) {
      // Defer formatting until composition ends
      return false;
    }
    return toggleMark(schema.marks.strong)(state, dispatch);
  }
};

// Or handle in beforeinput
view.dom.addEventListener('beforeinput', (e) => {
  if (e.inputType === 'formatBold' && e.isComposing) {
    e.preventDefault();
    // Handle composition-aware formatting
  }
});
  
  • View.composing: Checks view.composing to detect active composition.
  • Keymap handling: Can defer formatting in keymap when composition is active.
  • Transaction-based: Formats are applied as transactions, allowing better control.

Best Practices

  • Always check composition state: Use e.isComposing or view.composing before handling formatting.
  • Handle collapsed selection separately: Check selection.isCollapsed and implement toggle behavior for formatting state.
  • Listen to both events: Handle both beforeinput and input to catch all cases.
  • Test across browsers: Behavior varies significantly, especially for IME composition.
  • Maintain formatting state: Don't rely solely on DOM state; maintain your own formatting state for collapsed selections.

Mobile-Specific Input Types

Mobile devices and modern browsers support additional inputType values that are particularly relevant for mobile input methods:

Mobile Input Types

Paste & Drop

  • insertFromPaste - Content pasted from clipboard
  • insertFromDrop - Content dropped via drag-and-drop
  • insertFromYank - Content yanked (some browsers)

IME Composition

  • insertCompositionText - Text inserted during IME composition
  • deleteByComposition - Content deleted during IME composition

Auto-complete & Prediction

  • insertReplacementText - Text replaced by auto-complete or suggestion
  • insertFromPredictiveText - Predictive text insertion (some browsers)

Word-level Deletion

  • deleteWordBackward - Delete word backward (Ctrl+Backspace)
  • deleteWordForward - Delete word forward (Ctrl+Delete)

Other Deletion Types

  • deleteByCut - Content deleted via cut operation
  • deleteByDrag - Content deleted via drag operation

Note: Not all browsers support all inputType values. Mobile browsers, especially Android keyboards (like Samsung Keyboard), may trigger non-standard inputType values or use standard ones in unexpected ways.

See insertText - Mobile Keyboard Text Prediction for Android-specific behavior.

Testing & Debugging

To test these edge cases, use the playground and monitor both beforeinput and input events:

element.addEventListener('beforeinput', (e) => {
  console.log('beforeinput:', e.inputType);
  if (e.inputType === 'formatBold') {
    e.preventDefault(); // Can prevent default behavior
    // Apply custom formatting
  }
});

element.addEventListener('input', (e) => {
  console.log('input:', e.inputType);
  // DOM already updated, cannot prevent
});

Test scenarios:

  • Select text → Press Cmd/Ctrl + B (normal case)
  • Collapsed cursor → Press Cmd/Ctrl + B (collapsed case)
  • Start IME composition → Press Cmd/Ctrl + B (composition case)
  • Collapsed + IME composition → Press Cmd/Ctrl + B (worst case)

Related resources