Phenomenon
A specific regression in Chrome for Android (reported Dec 2025) involves an interaction between the CSS-based placeholder implementation and the Korean IME. When a contenteditable element is empty and displays a placeholder, starting a Korean composition session sometimes causes the browser to reset the DOM as it removes the placeholder, which in turn kills the active composition session for the first character.
Reproduction Steps
- Create a
contenteditableelement with aplaceholderattribute or a CSS:empty:beforerule. - Open the page in Chrome for Android (v131+).
- Focus the empty element.
- Input the first Jamo of a Korean syllable (e.g., “ㅎ”).
- Observe if the character remains or disappears.
Observed Behavior
compositionstart: Fires correctly.- Placeholder Logic: The browser detects the element is no longer empty and removes the
:beforeor internal placeholder node. - Collision: The DOM mutation for removing the placeholder interferes with the IME’s internal buffer for the first character.
- Result: The character “ㅎ” is lost, or the IME “stutters,” requiring the user to type the first character twice.
Expected Behavior
The placeholder removal should be an atomic operation that does not disrupt the active IME composition session. The first character should remain visible and combine correctly with subsequent input.
Impact
- Severe UX Frustration: Users constantly have to re-type the first letter of every new sentence or field.
- Data Corruption: In some cases, a partial syllable is left behind as “dead text” that the editor framework cannot clean up.
Browser Comparison
- Chrome for Android (v131/v132): Exhibits the bug.
- Firefox for Android: Works correctly.
- iOS Safari: Works correctly (uses a different placeholder injection method).
References & Solutions
Mitigation: Hide Placeholder on Focus
Instead of relying on :empty, hide the placeholder as soon as the element gains focus, before the composition starts.
/* Avoid this if possible for Korean IME */
[contenteditable]:empty::before {
content: attr(placeholder);
}
/* Better workaround */
[contenteditable]:focus::before {
content: "" !important; /* Remove placeholder immediately on focus */
}