execCommand alternatives

Modern alternatives to the deprecated execCommand API for formatting and editing operations.

⚠️ Deprecated - Do Not Use

The document.execCommand() API has been deprecated and should not be used in new code. It may stop working in future browser versions, and its behavior is inconsistent across browsers.

Always use modern alternatives: Selection API, Range API, and the beforeinput event with inputType instead.

execCommand to inputType Mapping

Many execCommand operations are now available as inputType values in the beforeinput and input events. This allows you to intercept and handle formatting operations before they're applied to the DOM.

Common execCommand to inputType Mappings

execCommand inputType Browser Behavior Keyboard Shortcut
bold formatBold Wraps selected text in <strong> or <b> element. If already bold, removes formatting. Ctrl/Cmd + B
italic formatItalic Wraps selected text in <em> or <i> element. Toggles italic formatting. Ctrl/Cmd + I
underline formatUnderline Wraps selected text in <u> element. Toggles underline formatting. Ctrl/Cmd + U
strikeThrough formatStrikeThrough Wraps selected text in <s> or <strike> element. Toggles strikethrough. No standard shortcut
createLink formatCreateLink Wraps selected text in <a href="..."> element. Prompts for URL if not provided. Ctrl/Cmd + K
insertUnorderedList insertUnorderedList Wraps selected paragraphs in <ul> with <li> items. Converts existing list if present. No standard shortcut
insertOrderedList insertOrderedList Wraps selected paragraphs in <ol> with <li> items. Converts existing list if present. No standard shortcut
formatBlock formatBlock Wraps selected content in block element (e.g., <h1>, <p>, <div>). Replaces existing block if selection spans entire block. No standard shortcut
insertHTML insertHTML Inserts HTML string at selection. Replaces selection if text is selected. Parses HTML and inserts as DOM nodes. N/A (programmatic only)
delete deleteContent Deletes selected content. If collapsed, deletes character after cursor (forward delete). Delete or Backspace
forwardDelete deleteContentForward Deletes character after cursor (forward delete). Equivalent to Delete key. Delete
undo historyUndo Undoes last operation. Browser maintains undo stack for contenteditable regions. Ctrl/Cmd + Z
redo historyRedo Redoes last undone operation. Browser maintains redo stack. Ctrl/Cmd + Shift + Z
selectAll selectAll Selects all content in the contenteditable element. Ctrl/Cmd + A
copy N/A (Clipboard API) Copies selected content to clipboard. Use navigator.clipboard.writeText() or Clipboard API instead. Ctrl/Cmd + C
cut N/A (Clipboard API) Cuts selected content to clipboard. Use navigator.clipboard.writeText() and range.deleteContents() instead. Ctrl/Cmd + X
paste insertFromPaste Pastes content from clipboard. Use navigator.clipboard.readText() or Clipboard API, then insert at selection. Ctrl/Cmd + V

Note: Not all execCommand operations have corresponding inputType values. Some operations (like copy, cut) should use the Clipboard API instead.

Using beforeinput with inputType

The beforeinput event fires before formatting operations are applied. You can intercept these events, prevent the default behavior, and implement your own formatting logic using the Selection API.

element.addEventListener('beforeinput', (e) => {
  if (e.inputType === 'formatBold') {
    e.preventDefault();
    // Apply bold formatting using Selection API
    applyBold();
  }
});

Editor-Specific Implementations

Different editor frameworks handle formatting operations differently. Here's how major editors implement bold formatting as an example:

Slate.js

Bold Formatting

Slate uses Transforms.setNodes() to toggle formatting marks on text nodes:

    import { Editor, Transforms } from 'slate';
import { ReactEditor } from 'slate-react';

function handleFormatBold(editor: Editor) {
  const isActive = isBoldActive(editor);
  Transforms.setNodes(
    editor,
    { bold: !isActive },
    { match: n => Text.isText(n), split: true }
  );
}

// In beforeinput handler
if (e.inputType === 'formatBold') {
  e.preventDefault();
  handleFormatBold(editor);
}
  
  • Mark-based: Formatting is stored as properties on text nodes (e.g., { bold: true }).
  • Transforms API: Uses Transforms.setNodes() to apply/remove formatting.
  • Serialization: Marks are converted to HTML elements (e.g., <strong>) during rendering.
ProseMirror

Bold Formatting

ProseMirror uses mark commands and keymaps to handle formatting:

    import { toggleMark } from 'prosemirror-commands';
import { schema } from './schema';

function handleFormatBold(state, dispatch) {
  return toggleMark(schema.marks.strong)(state, dispatch);
}

// In keymap
{
  'Mod-b': (state, dispatch) => {
    return handleFormatBold(state, dispatch);
  }
}
  
  • Mark system: Formatting is represented as marks in the document schema (e.g., schema.marks.strong).
  • Commands: Uses toggleMark() command to apply/remove marks.
  • Keymap: Keyboard shortcuts are defined in keymap plugins (e.g., Mod-b for bold).
Draft.js

Bold Formatting

Draft.js uses inline styles and RichUtils to handle formatting:

    import { RichUtils } from 'draft-js';

function handleFormatBold(editorState) {
  return RichUtils.toggleInlineStyle(editorState, 'BOLD');
}

// In key binding function
function myKeyBindingFn(e) {
  if (e.keyCode === 66 && (e.ctrlKey || e.metaKey)) {
    return 'bold';
  }
  return getDefaultKeyBinding(e);
}

// In key command handler
function handleKeyCommand(command) {
  if (command === 'bold') {
    const newState = handleFormatBold(editorState);
    setEditorState(newState);
    return 'handled';
  }
  return 'not-handled';
}
  
  • Inline styles: Formatting is stored as inline style strings (e.g., 'BOLD') in the ContentState.
  • RichUtils: Uses RichUtils.toggleInlineStyle() to toggle formatting.
  • Key bindings: Custom key binding function maps keyboard shortcuts to commands.

Using the Selection API

Instead of execCommand('bold'), you can wrap the selected text in a <strong> element using the Selection and Range APIs.

function applyBold() {
  const selection = window.getSelection();
  if (selection.rangeCount === 0) return;

  const range = selection.getRangeAt(0);
  const strong = document.createElement('strong');
  
  try {
    strong.appendChild(range.extractContents());
    range.insertNode(strong);
    selection.removeAllRanges();
    selection.addRange(range);
  } catch (e) {
    console.error('Failed to apply bold:', e);
  }
}

Platform-Specific Issues & Edge Cases

The behavior of beforeinput events and inputType values can vary significantly depending on browser, device, OS, and keyboard type. These variations can cause unexpected behavior when implementing execCommand alternatives.

Browser-Specific Issues

⚠️ Safari: beforeinput Limitations

Safari: While Safari supports beforeinput events, there may be differences in behavior compared to Chrome/Edge:

  • Some inputType values may not be supported or may behave differently
  • Event timing may differ, especially during IME composition
  • Formatting operations may fire in different order or with different properties
  • Older Safari versions may have limited or no support

Best Practice: Always test beforeinput behavior in Safari and provide fallbacks for unsupported inputType values.

⚠️ Chrome/Edge: Event Order Variations

Chrome/Edge: The order of beforeinput and compositionupdate events may differ from other browsers, especially during IME composition.

  • In some cases, compositionupdate may fire before beforeinput
  • This can cause timing issues when handling formatting during composition

⚠️ Firefox: Different inputType Values

Firefox: May use different inputType values or fire events in different orders than Chrome/Edge.

  • Some formatting operations may not have corresponding inputType values
  • Event timing may differ, especially for complex operations

OS & Keyboard-Specific Issues

⚠️ macOS + Korean IME: Formatting Events Don't Fire

macOS + Korean IME (2벌식, 3벌식, etc.): During IME composition with a collapsed cursor, pressing formatting shortcuts (e.g., Cmd + B, Cmd + I) may cause unexpected behavior:

  • formatBold, formatItalic, etc. do not fire
  • Instead, insertCompositionText may fire
  • Formatting is completely ignored during composition
  • User must commit composition first, then apply formatting

Impact: Users cannot apply formatting while composing Korean text, breaking expected workflow.

⚠️ Windows: IME Behavior Varies

Windows: IME behavior varies by Windows version and IME provider (Microsoft IME, Google IME, etc.).

  • Different IME engines may fire events at different times
  • Formatting shortcuts during composition may behave differently
  • Event timing can vary between Windows 10 and Windows 11

⚠️ Keyboard Layout Differences

Different keyboard layouts: Korean IME layouts (2벌식, 3벌식, 390 자판) may affect when formatting events fire.

  • Composition event timing differs between layouts
  • Formatting shortcuts may be intercepted differently
  • Event patterns can vary even for the same text input

Device-Specific Issues

⚠️ Mobile Keyboards: Text Prediction Interference

Mobile devices (Android/iOS): Virtual keyboards with text prediction features can interfere with formatting operations:

  • Android Samsung Keyboard: Text prediction may cause beforeinput to not fire before suggestion insertion
  • iOS QuickType: May use different inputType values (insertReplacementText vs insertFromPredictiveText)
  • Multiple input events may fire for a single suggestion
  • Formatting shortcuts may not work as expected on mobile keyboards
  • Physical keyboard attachments may behave differently than virtual keyboards

⚠️ Tablet vs Desktop

Tablets: Hybrid behavior between desktop and mobile:

  • External keyboard may behave like desktop
  • On-screen keyboard may behave like mobile
  • Event patterns can switch dynamically

Selection State Issues

⚠️ Collapsed Selection: Events May Not Fire

Collapsed selection (cursor only): When the selection is collapsed (no text selected), formatting operations may behave unexpectedly:

  • beforeinput may or may not fire (browser-dependent)
  • input typically does not fire
  • Browser may toggle "formatting state" for next character instead
  • This makes it difficult to detect when formatting is applied

⚠️ IME Composition + Collapsed Selection

Most unpredictable case: Collapsed selection during IME composition:

  • Formatting events may fire in unexpected order or not at all
  • Browser behavior varies significantly
  • macOS Korean IME: Formatting shortcuts completely ignored
  • Windows IME: May or may not fire formatting events

Best Practices for Handling Platform Differences

  • Detect browser capabilities: Check if beforeinput is supported before relying on it
  • Handle both events: Listen to both beforeinput and input to catch all cases
  • Check composition state: Always check isComposing flag before handling formatting
  • Test across platforms: Test with different browsers, OS, keyboards, and devices
  • Provide fallbacks: Use keydown events as fallback for Safari
  • Monitor DOM changes: Compare DOM state before/after events to detect what actually changed
  • Handle edge cases: Account for collapsed selections, IME composition, and mobile keyboards

Challenges

Complexity: The Selection API approach is more complex than execCommand and requires careful handling of edge cases (empty selections, collapsed ranges, cross-element selections, etc.).

Browser differences: Selection and Range behavior varies across browsers, especially when dealing with complex DOM structures, IME composition, or nested formatting.

No standard replacement: There is no single, standardized replacement for execCommand. Each operation must be implemented separately using the Selection API, and behavior may differ from execCommand's original implementation.

Related resources