Skip to content

Scroll "back" on web causes browser back action (unless perfectly horizontal) #152588

Closed

Description

@kevmoo

https://main-api.flutter.dev/flutter/material/CarouselView-class.html

If you navigate to this page from somewhere else and try to scroll the carousel view BACK you have to slide perfectly horizontally. Any deviation and you start the mac "back" action in the browser.

Look at https://dartpad.dev/?run=true&sample_id=material.CarouselView.2&channel=main

Flutter 3.24.0-1.0.pre.214

352322311-6cdc179e-ed34-4af7-a55b-ba127f8602ee.mov

I thought it was related to #143276 (comment) – but I'm being told...not

Activity

  1. kevmoo commented on Jul 30, 2024

    @kevmoo
    ContributorAuthor
  2. changed the title [-]`CarouselView`[/-] [+]`CarouselView` scroll "back" on web causes browser back action (unless perfectly horizontal)[/+] on Jul 30, 2024
  3. moffatman commented on Jul 30, 2024

    @moffatman
    Contributor

    Yeah idk. Maybe we need some e.preventDefault() somewhere

  4. added
    in triagePresently being triaged by the triage team
    platform-webWeb applications specifically
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    frameworkflutter/packages/flutter repository. See also f: labels.
    p: material_uimaterial_ui package in flutter/packages
    fyi-designFor the attention of Design Languages team
    team-webOwned by Web platform team
    and removed
    in triagePresently being triaged by the triage team
    on Jul 31, 2024
  5. kevmoo commented on Jul 31, 2024

    @kevmoo
    ContributorAuthor

    Weird that it acts FINE on native, BTW. I tried on Mac desktop.

  6. Piinks commented on Jul 31, 2024

    @Piinks
    Contributor

    Why is it weird? Desktop apps do not have a back gesture (which is conflicting with the scroll gesture) like web does AFAIK.

  7. removed
    fyi-designFor the attention of Design Languages team
    on Jul 31, 2024
  8. 35 remaining items

  9. AlexDochioiu commented on Mar 21, 2025

    @AlexDochioiu

    +1 . Issue still persists in flutter 3.29.2.

    Also, this seems to be the best fix (trade-off) for now:

    html, body {
      overscroll-behavior-x: none;
    }
  10. Michkwetzel commented on May 21, 2025

    @Michkwetzel

    Some Insight

    1. When attempting to scroll horizontally on a touchpad/touchscreen in a Flutter web app, the widget initially detects the gesture and moves slightly (see initial video and also on my own apps), but then the browser's navigation gesture takes over. This stops the scrolling action and triggers the browser's back/forward navigation instead.
    2. If you first swipe in a direction where the browser doesn't have navigation history (typically right-to-left for "forward" navigation), and then - without lifting your finger - reverse direction, the horizontal scrolling works just fine.
      This strongly suggests a gesture ownership conflict or the if there is navigation history the browser overlooks the flutter widget.

    Possible Cause
    The issue appears to be that Flutter initially detects and begins processing the horizontal gesture (as evidenced by the slight movement), but doesn't properly signal to the browser that it should override the default navigation behavior. The browser then "steals" control of the gesture from Flutter.
    This explains why the right-then-left pattern works: once Flutter establishes firm control of the gesture sequence by scrolling in one direction, it maintains that control when the direction changes.

    Proposed Solution Concept

    if (cursor over horizontally scrollable widget) {
        disable default browser navigation behavior
    }
    

    Flutter code needs to:

    1. Identify horizontally scrollable widgets
    2. Mark them with appropriate DOM attributes/tags
    3. Apply CSS that disables browser navigation gestures only for those elements

    Maybe I am completely wrong but this makes sense to me and hope this will provide further clarity on the issue. For now

      <style>
        /* Completely disable horizontal swiping gestures at the document level */
        html, body, flt-glass-pane, flt-scene-host {
          overscroll-behavior-x: none !important;
          touch-action: pan-y !important; /* Only allow vertical panning */
        }
        
        /* Target Flutter's render elements more specifically */
        flt-scene-host * {
          touch-action: pan-y !important;
        }
      </style>
    

    works

  11. NicolaVerbeeck commented on Jun 26, 2025

    @NicolaVerbeeck

    Is there anyway around this issue in the scenario that flutter is placed embedded in another top level page (especially if flutter is running in an iframe). Putting these styles on the iframe does not solve the behaviour.

  12. HeinrichJanzing commented on Aug 12, 2025

    @HeinrichJanzing

    I think this problem is related to the nesting of a horizontal scroll view in a vertical scroll view, which seems to be the case in the original Carrousel example, too.

    Looking at the implementation of ScrollableState._receivedPointerSignal:

      void _receivedPointerSignal(PointerSignalEvent event) {
        if (event is PointerScrollEvent && _position != null) {
          if (_physics != null && !_physics!.shouldAcceptUserOffset(position)) {
            // The handler won't use the `event`, so allow the platform to trigger
            // any default native actions.
    >>>     event.respond(allowPlatformDefault: true);
            return;
          }
          final double delta = _pointerSignalEventDelta(event);
          final double targetScrollOffset = _targetScrollOffsetForPointerScroll(delta);
          // Only express interest in the event if it would actually result in a scroll.
          if (delta != 0.0 && targetScrollOffset != position.pixels) {
            GestureBinding.instance.pointerSignalResolver.register(event, _handlePointerScroll);
            return;
          }
          // The `event` won't result in a scroll, so allow the platform to trigger
          // any default native actions.
    >>>   event.respond(allowPlatformDefault: true);
        } else if (event is PointerScrollInertiaCancelEvent) {
          position.pointerScroll(0);
          // Don't use the pointer signal resolver, all hit-tested scrollables should stop.
        }
      }

    On both marked lines, we allow default platform behavior if the scroll event would have no effect for us. Imagine a horizontal scroll view nested in a vertical one. A horizontal touchpad gesture is performed, resulting in a PointerScrollEvent with a scrollDelta.dx != 0 && scrollDelta.dy == 0. Both scroll views will receive the pointer signal. The horizontal one may deem it relevant and register with the pointerSignalResolver. The vertical one will deem it irrelevant and call event.respond(allowPlatformDefault: true), resulting in the undesired triggering of backnavigation.

    It seems to me no single widget should call event.respond(allowPlatformDefault: true) in a PointerSignalEvent listener, since the event could be relevant to another widget that is also receiving it. Based on the implementation of PointerSignalResolver.resolve, it seems to me that this explicit event.respond could be unnecessary. It is the default behavior if no one has claimed the PointerScrollEvent:

      void resolve(PointerSignalEvent event) {
        if (_firstRegisteredCallback == null) {
          assert(_currentEvent == null);
          // Nothing in the framework/app wants to handle the `event`. Allow the
          // platform to trigger any default native actions.
    >>>   event.respond(allowPlatformDefault: true);
          return;
        }
        assert(_isSameEvent(_currentEvent!, event));
        try {
          _firstRegisteredCallback!(_currentEvent!);

    If I just remove both marked lines from scrollable.dart the problem seems to be solved, but I'm sure they were there for some reason. @ditman since you introduced this functionality: any input on the reason for the additional explicit calls to event.respond in ScrollableState._receivedPointerSignal?

  13. lsimunec commented on Sep 18, 2025

    @lsimunec

    Reusable widget if someone needs to disable the web swipe back/forward gesture for certain widget/region.

    hover_disable_swipe_widget.dart

    import 'package:flutter/foundation.dart';
    import 'package:flutter/material.dart';
    
    import 'hover_no_swipe_stub.dart'
        if (dart.library.html) 'hover_no_swipe_web.dart';
    
    class HoverDisableSwipeWidget extends StatelessWidget {
      final Widget child;
    
      const HoverDisableSwipeWidget({super.key, required this.child});
    
      @override
      Widget build(BuildContext context) {
        return kIsWeb
            ? MouseRegion(
                onEnter: (_) => addNoSwipeCSS(),
                onExit: (_) => removeNoSwipeCSS(),
                child: child,
              )
            : child;
      }
    }
    

    hover_no_swipe_stub.dart

    void addNoSwipeCSS() {}
    void removeNoSwipeCSS() {}
    

    hover_no_swipe_web.dart

    // Only compiled for web
    // ignore: depend_on_referenced_packages
    import 'package:web/web.dart' as web;
    
    void addNoSwipeCSS() {
      final document = web.document;
      final head = document.head;
      if (head != null) {
        final existing = document.getElementById('no-swipe-navigation');
        existing?.remove();
    
        final styleElement =
            document.createElement('style') as web.HTMLStyleElement;
        styleElement.id = 'no-swipe-navigation';
        styleElement.textContent = '''
          html, body {
            overscroll-behavior-x: none;
            touch-action: pan-y;
          }
          * {
            overscroll-behavior-x: none;
          }
        ''';
        head.appendChild(styleElement);
      }
    }
    
    void removeNoSwipeCSS() {
      final existing = web.document.getElementById('no-swipe-navigation');
      existing?.remove();
    }
    
  14. O-Hannonen commented on Mar 6, 2026

    @O-Hannonen
    Contributor

    Did some pretty thorough investigations and came to the same conclusion as @HeinrichJanzing. I really could not find any reason why Scrollable should manually signal resolve(true) from its local state, so I deemed it as oversight in the original PR #145500. @ditman feel free to correct if this assumption is wrong 😅

    Wrote down quite detailed reasoning to the PR, but a small recap here too:
    Reverting the changes from PR #145500 that added these manual respond calls will not regress the original issue (#139263). The PointerSignalResolver.resolve method already calls event.respond(allowPlatformDefault: true) when no callback is registered. By removing the eager calls in Scrollable, we allow the resolver to wait until the entire hit-test path is processed, ensuring that if any widget (like a horizontal carousel) handles the event, the engine is correctly notified that the framework has claimed the gesture, thus preventing the browser's default navigation behavior.

  15. added a commit that references this issue on Mar 19, 2026
    3d27a22
  16. ditman commented on Mar 20, 2026

    @ditman
    Member

    Wrote down quite detailed reasoning to the PR, but a small recap here too: Reverting the changes from PR #145500 that added these manual respond calls will not regress the original issue (#139263). The PointerSignalResolver.resolve method already calls event.respond(allowPlatformDefault: true) when no callback is registered. By removing the eager calls in Scrollable, we allow the resolver to wait until the entire hit-test path is processed, ensuring that if any widget (like a horizontal carousel) handles the event, the engine is correctly notified that the framework has claimed the gesture, thus preventing the browser's default navigation behavior.

    Oh my! I haven't worked in this for a very long time, so my opinion doesn't matter much, but I'm sure your fix is appropriate! Thanks for fixing this! Looking forward to testing carousels on the web again!

  17. added a commit that references this issue on Mar 26, 2026
    fc0a42e
  18. github-actions commented on Apr 3, 2026

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  19. locked as resolved and limited conversation to collaborators on Apr 3, 2026
  20. added a commit that references this issue on Apr 14, 2026
    90f9b0e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listf: gesturesflutter/packages/flutter/gestures repository.f: scrollingViewports, list views, slivers, etc.found in release: 3.24Found to occur in 3.24frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onp: material_uimaterial_ui package in flutter/packagesplatform-webWeb applications specificallyteam-webOwned by Web platform teamtriaged-webTriaged by Web platform teamworkaround availableThere is a workaround available to overcome the issue

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions