Issue #156985: Scroll Events Bubble to Parent Page When Flutter is Embedded in iframe

Problem Description

When a Flutter web app is embedded in an iframe, scroll events (mouse wheel) inside the Flutter app bubble up to the parent/host page, causing both the Flutter content and the parent page to scroll simultaneously.

User Experience Impact

  • Users scrolling through Flutter content also inadvertently scroll the parent page
  • Creates a jarring, confusing experience
  • Makes embedded Flutter apps feel broken compared to native iframe content

Reproduction Steps

  1. Embed a Flutter web app in an iframe on a scrollable HTML page
  2. Scroll inside the Flutter app using mouse wheel
  3. Observe that the parent page also scrolls

Root Cause Analysis

The issue stems from how browsers handle scroll event propagation:

  1. Native Browser Scroll Chaining: By default, when an element inside an iframe reaches its scroll boundary, the browser propagates the scroll to the parent document (“scroll chaining”)

  2. Flutter's allowPlatformDefault: Flutter uses event.respond(allowPlatformDefault: true/false) to control whether the browser should handle scroll events. When a scrollable is at its boundary, it sets allowPlatformDefault: true, which allows the browser's native scroll chaining to occur.

  3. Nested Scrollables Problem: With nested scrollables (e.g., ListView inside SingleChildScrollView), when the inner scrollable hits its boundary, it says “allow platform default”. But the outer scrollable might still be able to scroll! The engine was using |= to combine responses, so once ANY widget said “allow”, it would stay “allow”.

Solution

Engine Changes (pointer_binding.dart)

  1. Always preventDefault() in iframes: When Flutter detects it's running inside an iframe, it always calls preventDefault() on wheel events to block native scroll chaining.

  2. New flag _lastWheelEventHandledByWidget: Tracks whether ANY widget explicitly handled the scroll event (responded with allowPlatformDefault: false).

  3. Explicit parent scrolling via postMessage: When ALL Flutter scrollables are at their boundaries (no widget handled the event), the engine explicitly scrolls the parent page using window.parent.postMessage().

  4. Iframe detection: Added _isEmbeddedInIframe() method to detect if Flutter is running inside an iframe by checking if window.parent !== window.

// Key logic in _handleWheelEvent
final bool isInIframe = _isEmbeddedInIframe();
if (!_lastWheelEventAllowedDefault || isInIframe) {
  event.preventDefault();  // Block native scroll chaining
}

// Only scroll parent when ALL scrollables are at boundary
final bool shouldScrollParent = isInIframe && 
    _lastWheelEventAllowedDefault && 
    !_lastWheelEventHandledByWidget;
if (shouldScrollParent) {
  scrollParentWindow(wheelEvent.deltaX, wheelEvent.deltaY);
}

Framework Changes (scrollable.dart)

Added explicit respond(allowPlatformDefault: false) when a scrollable successfully handles a scroll event:

void _handlePointerScroll(PointerEvent event) {
  // ... scroll handling ...
  if (delta != 0.0 && targetScrollOffset != position.pixels) {
    position.pointerScroll(delta);
    // Tell engine this scrollable handled the event
    scrollEvent.respond(allowPlatformDefault: false);
  }
}

Engine Changes (dom.dart)

Added scrollParentWindow() function to scroll the parent page via postMessage:

void scrollParentWindow(double deltaX, double deltaY) {
  final JSAny jsMessage = <String, dynamic>{
    'type': 'flutter-scroll',
    'deltaX': deltaX,
    'deltaY': deltaY,
  }.jsify()!;
  domWindow.parent._postMessage(jsMessage, '*');
}

Files Changed

FileChange
engine/src/flutter/lib/web_ui/lib/src/engine/pointer_binding.dartIframe detection, preventDefault in iframe, parent scroll via postMessage
engine/src/flutter/lib/web_ui/lib/src/engine/dom.dartAdded scrollParentWindow(), parent property on DomWindow
packages/flutter/lib/src/widgets/scrollable.dartAdded respond(allowPlatformDefault: false) when scroll handled

Host Page Requirements

The host page must listen for flutter-scroll messages to receive scroll propagation:

window.addEventListener('message', function(event) {
  if (event.data && event.data.type === 'flutter-scroll') {
    window.scrollBy(event.data.deltaX, event.data.deltaY);
  }
});

Demo

Behavior After Fix

  1. ✅ Scrolling inside Flutter iframe does NOT scroll parent page
  2. ✅ When Flutter content reaches boundary, parent page scrolls
  3. ✅ Nested scrollables work correctly - outer scrollable handles event before parent page
  4. ✅ Works with both same-origin and cross-origin iframes