Scenario

IME composition text leaks outside table cells

A technical evaluation of why IME composition often fails when anchored inside empty table structures.

ime
Scenario ID
scenario-table-composition-leaks

Details

Problem Overview

Table cells (<td>, <th>) are unique in the DOM because they act as both layout containers and structural boundaries. IME (Input Method Editor) sessions require a stable Selection range to manage the “pre-edit” text. In many browsers, particularly WebKit-based ones, if a cell is empty or contains only a <br> or a zero-width space, the engine’s internal selection-mapping logic can “overshoot” the cell boundary during the commit phase, placing the final text in the parent row or before the table entirely.

Observed Behavior

Scenario 1: The “Empty Cell” Trap

In an empty cell, the browser often represents the caret as being “at the start of the cell”, but logically it might resolve to the same point as “before the table”.

/* Observed Sequence */
// 1. User focuses <td></td>
// 2. User types 'G' (IME start)
// 3. Browser creates a temp span for 'G'
// 4. User presses Enter (Commit)
// 5. Browser destroys temp span
// 6. Browser inserts 'G' but uses a cached range that is no longer validly nested.

Impact

  • Document Corruption: The table becomes physically broken in the DOM.
  • Accessibility: Screen readers lose the context that the text belongs to a specific cell.
  • Styling: Text outside the <td> ignores cell padding, alignment, and scoping rules.

Browser Comparison

  • WebKit (Safari): Most vulnerable engine. The issue is persistent across iOS and macOS in versions 17.x.
  • Blink (Chrome): Generally robust due to an “automatic paragraph” insertion logic that adds a <p> or <br> to maintain cell height/focus.
  • Gecko (Firefox): Handles the boundary well, but can sometimes suffer from duplicate characters if the commit timing is slightly off.

Solutions

1. Invisible Placeholder (Zero-Width Space)

Ensuring the cell is never technically “empty” is the most common fix.

function ensureTdContent(td) {
  if (td.childNodes.length === 0) {
    // Add a ZWSP to anchor the selection
    td.appendChild(document.createTextNode('\u200B'));
  }
}

2. Manual Reconciliation on compositionend

Override the browser’s insertion by intercepting the final commit.

element.addEventListener('compositionend', (e) => {
  const selection = window.getSelection();
  const range = selection.getRangeAt(0);
  const td = findParentTd(range.startContainer);
  
  if (!td) {
    e.preventDefault();
    console.warn('Fixing selection leak...');
    forceInsertIntoCorrectCell(e.data);
  }
});

Best Practices

  • Always maintain a ZWSP or BR: Never leave a contenteditable table cell completely empty.
  • Verify range containment: In your event handlers, always check cell.contains(range.commonAncestorContainer).

References

Scenario flow

Visual view of how this scenario connects to its concrete cases and environments. Nodes can be dragged and clicked.

React Flow mini map

Variants

Each row is a concrete case for this scenario, with a dedicated document and playground.

Case OS Device Browser Keyboard Status
ce-0566-safari-table-cell-composition-leak macOS 14.4 Desktop Any Safari 17.4 Japanese IME confirmed
ce-0575-prosemirror-safari-empty-table-leak macOS 15.2 Desktop Any Safari 18.2 Japanese IME confirmed

Browser compatibility

This matrix shows which browser and OS combinations have documented cases for this scenario. Click on a cell to view the specific case.

Confirmed
Draft
No case documented

Cases

Open a case to see the detailed description and its dedicated playground.

Related Scenarios

Other scenarios that share similar tags or category.

Tags: composition, selection

Selection mismatch between beforeinput and input events

The selection (window.getSelection()) in beforeinput events can differ from the selection in corresponding input events. This mismatch can occur during IME composition, text prediction, or when typing adjacent to formatted elements like links. The selection in beforeinput may include adjacent formatted text, while input selection reflects the final cursor position.

1 case
Tags: composition, selection

getTargetRanges() returns empty array in beforeinput events

The getTargetRanges() method in beforeinput events may return an empty array or undefined in various scenarios, including text prediction, certain IME compositions, or specific browser/device combinations. When getTargetRanges() is unavailable, developers must rely on window.getSelection() as a fallback, but this may be less accurate.

1 case
Tags: composition, webkit

insertParagraph preventDefault breaks IME composition state in Safari

In Safari desktop, when preventDefault() is called on keydown or beforeinput events for insertParagraph (Enter key), the IME composition state becomes corrupted. Subsequent text input fails to trigger proper input events, causing characters to not be inserted or composition to malfunction.

3 cases

Comments & Discussion

Have questions, suggestions, or want to share your experience? Join the discussion below.