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
beforeinputorinput
Editor Internal Models & DOM Synchronization
⚠️ Critical Exception: preventDefault() and Internal Models
When editors use internal models and preventDefault():
inputTypeevents may fire, but editors intercept them withpreventDefault()- 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
historyUndoevent 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,
insertCompositionTextevent 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
beforeinputfires (noinputevent) - 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)
beforeinputfires withinputType- You can call
e.preventDefault() inputfires after DOM is updated (if not prevented)
IME Composition Active
beforeinputmay not fire for formatting shortcutsinputfires directly (cannot prevent)- Browser applies default formatting
Collapsed Selection
beforeinputmay or may not fire (browser-dependent)inputtypically does not fire- Browser may toggle "formatting state" for next character
Collapsed + IME Composition
- Most unpredictable case
- Events may fire in unexpected order or not at all
- Browser behavior varies significantly
Mobile Text Prediction (Android Samsung Keyboard)
beforeinputmay not fire before suggestion insertion- Multiple
inputevents may fire for a single suggestion - Suggested text inserted as bulk (full phrase) rather than character-by-character
- Event timing differs from manual typing
Browser-Specific Behavior
Known Browser Differences
Chrome/Edge
- During IME composition:
beforeinputmay not fire forformatBold - Collapsed selection:
inputdoes not fire, onlybeforeinput - 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
inputTypevalues
Chrome on Android (Samsung Keyboard)
- Text prediction/suggestion:
beforeinputmay not fire before suggestion insertion - Suggested text inserted as bulk, causing multiple
inputevents - 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:
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.isComposingbefore handling formatBold. - Deferred formatting: Can store formatting intent and apply after composition ends.
- State management: Maintains formatting state separately from DOM.
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.composingto 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.isComposingorview.composingbefore handling formatting. - Handle collapsed selection separately: Check
selection.isCollapsedand implement toggle behavior for formatting state. - Listen to both events: Handle both
beforeinputandinputto 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 clipboardinsertFromDrop- Content dropped via drag-and-dropinsertFromYank- Content yanked (some browsers)
IME Composition
insertCompositionText- Text inserted during IME compositiondeleteByComposition- Content deleted during IME composition
Auto-complete & Prediction
insertReplacementText- Text replaced by auto-complete or suggestioninsertFromPredictiveText- 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 operationdeleteByDrag- 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)