Case ce-0293-ios-dictation-duplicate-events-safari · Scenario scenario-ios-dictation-duplicate-events

iOS dictation re-fires input events with text split into words

OS: iOS 17.0+ Device: iPhone or iPad Any Browser: Safari 17.0+ Keyboard: Voice Dictation Status: draft
dictation voice-input beforeinput input ios safari duplicate-events sync-issue

Phenomenon

On iOS Safari, when using voice dictation to input text into contenteditable elements, the system fires initial beforeinput and input events with the complete dictated text. After the initial input completes, the system re-fires beforeinput and input events with the text split into individual words, causing event handlers to execute multiple times for the same input.

Reproduction example

  1. Open a web page with a contenteditable element on iOS Safari (iPhone or iPad).
  2. Focus the contenteditable element.
  3. Activate voice dictation (long press spacebar or tap microphone icon on keyboard).
  4. Dictate text: “만나서 반갑습니다” (or any multi-word phrase).
  5. Observe the beforeinput and input events in the browser console or event log.

Observed behavior

Initial Dictation Sequence

  1. User activates dictation and speaks “만나서 반갑습니다”
  2. beforeinput event fires with:
    • inputType: 'insertText'
    • data: '만나서 반갑습니다'
    • isComposing: false
  3. input event fires with the complete text “만나서 반갑습니다” inserted into DOM

Duplicate Events Sequence (Bug)

  1. After a short delay (typically 100-500ms), beforeinput event fires again with:
    • inputType: 'insertText'
    • data: '만나서'
    • isComposing: false
  2. input event fires with “만나서” inserted
  3. beforeinput event fires again with:
    • inputType: 'insertText'
    • data: ' ' (space character)
    • isComposing: false
  4. input event fires with space inserted
  5. beforeinput event fires again with:
    • inputType: 'insertText'
    • data: '반갑습니다'
    • isComposing: false
  6. input event fires with “반갑습니다” inserted

Key Characteristics

  • Composition events (compositionstart, compositionupdate, compositionend) do NOT fire during dictation
  • isComposing is always false in all events
  • Events are re-fired after the initial input completes
  • Text is split at word boundaries (spaces)
  • The DOM state after duplicate events is the same as after initial events (no actual change)
  • Event sequence becomes out of sync with DOM state

Expected behavior

  • Initial beforeinput and input events should fire once with the complete dictated text
  • Events should NOT be re-fired after completion
  • If events are re-fired, they should reflect actual DOM changes (not duplicate insertions)
  • Event sequence should remain synchronized with DOM state
  • Composition events should fire during dictation (as they do on macOS Safari)

Impact

This can lead to:

  • Duplicate processing: Event handlers execute multiple times for the same input
  • State synchronization issues: Application state may become inconsistent with DOM state
  • Performance issues: Unnecessary processing of duplicate events
  • Undo/redo corruption: Undo stack may contain duplicate or incorrect entries
  • Validation issues: Validation logic may run multiple times on the same input
  • Formatting issues: Formatting logic may be applied incorrectly due to split text
  • Event sequence confusion: Handlers expecting a single input event receive multiple events

Browser Comparison

  • iOS Safari: Dictation does not fire composition events, events are re-fired after completion with text split into words
  • iOS Chrome: Same behavior as Safari (uses WebKit engine, required by Apple)
  • macOS Safari: Dictation fires composition events, events are not re-fired after completion
  • Chrome/Edge/Firefox (Desktop): Dictation behavior varies but generally more consistent, no duplicate re-firing

Distinguishing Dictation Input

Important: There is no reliable way to detect dictation input in web applications on iOS. Web APIs do not provide dictation detection capabilities, and native iOS APIs like UITextInputContext.isDictationInputExpected are not available in web contexts.

Potential Indicators (Not Reliable)

  • Absence of composition events (but this also occurs with Korean IME on iOS)
  • Rapid insertion of multiple words
  • Text appears to be split and re-inserted
  • Events fire in quick succession with complete words
  • isComposing is always false (but this is also true for Korean IME on iOS)

These indicators are not definitive and may produce false positives.

Event Sequence

The sequence of events when inputting “만나서 반갑습니다” via dictation:

Phase 1: Initial Dictation Input

OrderEventinputTypedataDOM State (before)DOM State (after)
1beforeinputinsertText’만나서 반갑습니다’""-
2inputinsertText’만나서 반갑습니다’"""만나서 반갑습니다” ✅

Phase 2: Duplicate Events (Bug)

After initial input completes, after a delay of approximately 100-500ms, events are re-fired with text split into words:

OrderEventinputTypedataDOM State (before)DOM State (after)
3beforeinputinsertText’만나서‘“만나서 반갑습니다”-
4inputinsertText’만나서‘“만나서 반갑습니다""만나서 반갑습니다” ❌
5beforeinputinsertText’ ‘“만나서 반갑습니다”-
6inputinsertText’ ‘“만나서 반갑습니다""만나서 반갑습니다” ❌
7beforeinputinsertText’반갑습니다‘“만나서 반갑습니다”-
8inputinsertText’반갑습니다‘“만나서 반갑습니다""만나서 반갑습니다” ❌

Key Characteristics

  • Events 1-2: Complete text inserted at once (DOM actually changes)
  • Events 3-8: Text re-fired word-by-word but DOM doesn’t change
  • Composition events: compositionstart, compositionupdate, compositionend events do NOT fire in any phase
  • isComposing: All events have isComposing: false
  • Delay between phases: 100-500ms delay between Event 2 and Event 3

Complete Event Monitoring

Code to monitor all events during iOS dictation input:

const element = document.querySelector('[contenteditable]');
const eventLog = [];

const eventsToMonitor = [
  'compositionstart', 'compositionupdate', 'compositionend',
  'beforeinput', 'input',
  'keydown', 'keyup', 'keypress'
];

eventsToMonitor.forEach(eventType => {
  element.addEventListener(eventType, (e) => {
    const eventData = {
      timestamp: Date.now(),
      type: eventType,
      inputType: e.inputType || null,
      data: e.data || null,
      isComposing: e.isComposing || false,
      textContent: element.textContent
    };
    eventLog.push(eventData);
    console.log(`[${eventType}]`, eventData);
  }, { capture: true });
});

Events That Fire vs Do Not Fire

Event TypeFires?Initial InputDuplicate Events
beforeinput✅ Yes1 time3 times
input✅ Yes1 time3 times
compositionstart❌ No--
compositionupdate❌ No--
compositionend❌ No--
keydown❌ No--
keyup❌ No--
keypress❌ No--

Notes and possible direction for workarounds

Event Handling Considerations

  • Event handlers may execute multiple times for the same input
  • Events without actual DOM changes (Events 4, 6, 8) should not be processed
  • Check textContent to determine if DOM actually changed

Undo/Redo Stack

  • Recording duplicate events in undo stack creates duplicate undo entries
  • Only record in undo stack when there’s an actual DOM change

Additional Considerations

Selection State

  • Selection state may be reset when duplicate events fire, so don’t use selection for duplicate detection; trust only textContent

Undo/Redo Stack

  • Recording duplicate events in the undo stack creates duplicate undo entries
  • Using textContent-based deduplication ensures only actual changes are recorded in the undo stack

Voice Control Simultaneous Use

  • Enabling both Voice Control and Dictation in iOS settings may cause text to be inserted twice
  • This case actually changes the DOM, so textContent-based deduplication won’t detect it
  • Recommend users enable only one

Test Environment

iOS VersionBrowserLanguageReproduced
iOS 16.xSafariKorean✅ Confirmed
iOS 16.xSafariEnglish✅ Confirmed
iOS 16.xChrome iOSKorean✅ Confirmed
iOS 17.xSafariKorean✅ Confirmed
iOS 17.xSafariEnglish✅ Confirmed
iOS 17.xChrome iOSKorean✅ Confirmed
iOS 18.xSafariKorean⚠️ Unconfirmed
iOS 18.xSafariEnglish⚠️ Unconfirmed

Note: The same issue likely occurs across all iOS versions (shared WebKit engine). Issue appears to occur regardless of language.

Initial dictation
만나서 반갑습니다
User dictates '만나서 반갑습니다' - initial events fire with complete text
After duplicate events (Bug)
만나서 반갑습니다
Same text but events fire again as '만나서' + space + '반갑습니다'
vs
✅ Expected
만나서 반갑습니다
Expected: Events fire only once with complete text, no re-firing

Playground for this case

Use the reported environment as a reference and record what happens in your environment while interacting with the editable area.

Reported environment
OS: iOS 17.0+
Device: iPhone or iPad Any
Browser: Safari 17.0+
Keyboard: Voice Dictation
Your environment
Sample HTML:
Event log
Use this log together with the case description when filing or updating an issue.
0 events
Interact with the editable area to see events here.

Comments & Discussion

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