Skip to content
Boring Doc
BreakingHigh impactOfficial migration guidesince 3.3.0landed 3.3.0-0.0.pre

Trackpad gestures can trigger GestureRecognizer

Framework · Official source

Trackpad gestures on most platforms now send `PointerPanZoom` sequences and can trigger pan, drag, and scale `GestureRecognizer` callbacks.

Original behavior

Scrolling on Flutter Desktop prior to version 3.3.0 used `PointerScrollEvent` messages to represent discrete scroll deltas. This system worked well for mouse scroll wheels, but wasn't a good fit for trackpad scrolling. Trackpad scrolling is expected to cause momentum, which depends not only on the scroll deltas, but also the timing of when fingers are released from the trackpad. In addition, trackpad pinching-to-zoom could not be represented.

New behavior

The Flutter engine has been updated on all possible platforms to recognize trackpad gestures and send them to the framework as `PointerPanZoom` events instead of as `PointerScrollSignal` events. `PointerScrollSignal` events will still be used to represent scrolling on a mouse wheel.

Who is affected
Applications and packages relying on the previous behaviour or signature. Code that uses PointerPanZoom, GestureRecognizer, PointerScrollEvent and related APIs.
Practical impact
Behaviour or API differences that can break builds or change runtime behaviour.
What action should I take?
Follow the official migration guide below, update affected call sites, run `dart fix --apply` where a fix is available, then run your tests.

Before and after (from the official guide)

dart+1−0
Before
1 void main() => runApp(Foo());
2  
3 class Foo extends StatefulWidget {
4 late final PanGestureRecognizer recognizer;
5  
6 @override
7 void initState() {
8 super.initState();
9 recognizer = PanGestureRecognizer()
10 ..onStart = _onPanStart
11 ..onUpdate = _onPanUpdate
12 ..onEnd = _onPanEnd;
13 }
14  
15 void _onPanStart(DragStartDetails details) {
16 debugPrint('onStart');
17 }
18  
19 void _onPanUpdate(DragUpdateDetails details) {
20 debugPrint('onUpdate');
21 }
22  
23 void _onPanEnd(DragEndDetails details) {
24 debugPrint('onEnd');
25 }
26  
27 @override
28 Widget build(BuildContext context) {
29 return Listener(
30 onPointerDown: recognizer.addPointer,
 
31 child: Container()
32 );
33 }
34 }
After
1 void main() => runApp(Foo());
2  
3 class Foo extends StatefulWidget {
4 late final PanGestureRecognizer recognizer;
5  
6 @override
7 void initState() {
8 super.initState();
9 recognizer = PanGestureRecognizer()
10 ..onStart = _onPanStart
11 ..onUpdate = _onPanUpdate
12 ..onEnd = _onPanEnd;
13 }
14  
15 void _onPanStart(DragStartDetails details) {
16 debugPrint('onStart');
17 }
18  
19 void _onPanUpdate(DragUpdateDetails details) {
20 debugPrint('onUpdate');
21 }
22  
23 void _onPanEnd(DragEndDetails details) {
24 debugPrint('onEnd');
25 }
26  
27 @override
28 Widget build(BuildContext context) {
29 return Listener(
30 onPointerDown: recognizer.addPointer,
31+ onPointerPanZoomStart: recognizer.addPointerPanZoom,
32 child: Container()
33 );
34 }
35 }
Raw before / after
void main() => runApp(Foo());

class Foo extends StatefulWidget {
  late final PanGestureRecognizer recognizer;

  @override
  void initState() {
    super.initState();
    recognizer = PanGestureRecognizer()
    ..onStart = _onPanStart
    ..onUpdate = _onPanUpdate
    ..onEnd = _onPanEnd;
  }

  void _onPanStart(DragStartDetails details) {
    debugPrint('onStart');
  }

  void _onPanUpdate(DragUpdateDetails details) {
    debugPrint('onUpdate');
  }

  void _onPanEnd(DragEndDetails details) {
    debugPrint('onEnd');
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerDown: recognizer.addPointer,
      child: Container()
    );
  }
}
void main() => runApp(Foo());

class Foo extends StatefulWidget {
  late final PanGestureRecognizer recognizer;

  @override
  void initState() {
    super.initState();
    recognizer = PanGestureRecognizer()
    ..onStart = _onPanStart
    ..onUpdate = _onPanUpdate
    ..onEnd = _onPanEnd;
  }

  void _onPanStart(DragStartDetails details) {
    debugPrint('onStart');
  }

  void _onPanUpdate(DragUpdateDetails details) {
    debugPrint('onUpdate');
  }

  void _onPanEnd(DragEndDetails details) {
    debugPrint('onEnd');
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerDown: recognizer.addPointer,
      onPointerPanZoomStart: recognizer.addPointerPanZoom,
      child: Container()
    );
  }
}
Official migration guide (excerpt, © The Flutter Authors, CC BY 3.0)

Migration steps depend on whether you want each gesture interaction in your app to be usable via a trackpad, or whether it should be restricted to only touch and mouse usage.

For gesture interactions suitable for trackpad usage

Using GestureDetector

No change is needed, GestureDetector automatically processes trackpad gesture events and triggers callbacks if recognized.

Using GestureRecognizer and Listener

Ensure that onPointerPanZoomStart is passed through to each recognizer from the Listener. The addPointerPanZoom method of `GestureRecognizer must be called for it to show interest and start tracking each trackpad gesture.

Code before migration:

void main() => runApp(Foo());

class Foo extends StatefulWidget {
  late final PanGestureRecognizer recognizer;

  @override
  void initState() {
    super.initState();
    recognizer = PanGestureRecognizer()
    ..onStart = _onPanStart
    ..onUpdate = _onPanUpdate
    ..onEnd = _onPanEnd;
  }

  void _onPanStart(DragStartDetails details) {
    debugPrint('onStart');
  }

  void _onPanUpdate(DragUpdateDetails details) {
    debugPrint('onUpdate');
  }

  void _onPanEnd(DragEndDetails details) {
    debugPrint('onEnd');
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerDown: recognizer.addPointer,
      child: Container()
    );
  }
}

Code after migration:

void main() => runApp(Foo());

class Foo extends StatefulWidget {
  late final PanGestureRecognizer recognizer;

  @override
  void initState() {
    super.initState();
    recognizer = PanGestureRecognizer()
    ..onStart = _onPanStart
    ..onUpdate = _onPanUpdate
    ..onEnd = _onPanEnd;
  }

  void _onPanStart(DragStartDetails details) {
    debugPrint('onStart');
  }

  void _onPanUpdate(DragUpdateDetails details) {
    debugPrint('onUpdate');
  }

  void _onPanEnd(DragEndDetails details) {
    debugPrint('onEnd');
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerDown: recognizer.addPointer,
      onPointerPanZoomStart: recognizer.addPointerPanZoom,
      child: Container()
    );
  }
}

Using raw Listener

The following code using PointerScrollSignal will no longer be called upon all desktop scrolling. PointerPanZoomUpdate events should be captured to receive trackpad gesture data.

Code before migration:

void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerSignal: (PointerSignalEvent event) {
        if (event is PointerScrollEvent) {
          debugPrint('scroll wheel event');
        }
      }
      child: Container()
    );
  }
}

Code after migration:

void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerSignal: (PointerSignalEvent event) {
        if (event is PointerScrollEvent) {
          debugPrint('scroll wheel event');
        }
      },
      onPointerPanZoomUpdate: (PointerPanZoomUpdateEvent event) {
        debugPrint('trackpad scroll event');
      }
      child: Container()
    );
  }
}

Please note: Use of raw Listener in this way could cause conflicts with other gesture interactions as it doesn't participate in the gesture disambiguation arena.

For gesture interactions not suitable for trackpad usage

Using GestureDetector

If using Flutter 3.3.0, RawGestureDetector could be used instead of GestureDetector to ensure each GestureRecognizer created by the GestureDetector has supportedDevices set to exclude PointerDeviceKind.trackpad. Starting in version 3.4.0, there is a supportedDevices parameter directly on GestureDetector.

Code before migration:

void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanStart: (details) {
        debugPrint('onStart');
      },
      onPanUpdate: (details) {
        debugPrint('onUpdate');
      },
      onPanEnd: (details) {
        debugPrint('onEnd');
      }
      child: Container()
    );
  }
}

Code after migration (Flutter 3.3.0):

// Example of code after the change.
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RawGestureDetector(
      gestures: {
        PanGestureRecognizer:
            GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
          () => PanGestureRecognizer(
            supportedDevices: {
              PointerDeviceKind.touch,
              PointerDeviceKind.mouse,
              PointerDeviceKind.stylus,
              PointerDeviceKind.invertedStylus,
              // Do not include PointerDeviceKind.trackpad
            }
          ),
          (recognizer) {
            recognizer
              ..onStart = (details) {
                debugPrint('onStart');
              }
              ..onUpdate = (details) {
                debugPrint('onUpdate');
              }
              ..onEnd = (details) {
                debugPrint('onEnd');
              };
          },
        ),
      },
      child: Container()
    );
  }
}

Code after migration: (Flutter 3.4.0):

void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      supportedDevices: {
        PointerDeviceKind.touch,
        PointerDeviceKind.mouse,
        PointerDeviceKind.stylus,
        PointerDeviceKind.invertedStylus,
        // Do not include PointerDeviceKind.trackpad
      },
      onPanStart: (details) {
        debugPrint('onStart');
      },
      onPanUpdate: (details) {
        debugPrint('onUpdate');
      },
      onPanEnd: (details) {
        debugPrint('onEnd');
      }
      child: Container()
    );
  }
}

Using RawGestureRecognizer

Explicitly ensure that supportedDevices doesn't include PointerDeviceKind.trackpad.

Code before migration:

void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RawGestureDetector(
      gestures: {
        PanGestureRecognizer:
            GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
          () => PanGestureRecognizer(),
          (recognizer) {
            recognizer
              ..onStart = (details) {
                debugPrint('onStart');
              }
              ..onUpdate = (details) {
                debugPrint('onUpdate');
              }
              ..onEnd = (details) {
                debugPrint('onEnd');
              };
          },
        ),
      },
      child: Container()
    );
  }
}

Code after migration:

// Example of code after the change.
void main() => runApp(Foo());

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RawGestureDetector(
      gestures: {
        PanGestureRecognizer:
            GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
          () => PanGestureRecognizer(
            supportedDevices: {
              PointerDeviceKind.touch,
              PointerDeviceKind.mouse,
              PointerDeviceKind.stylus,
              PointerDeviceKind.invertedStylus,
              // Do not include PointerDeviceKind.trackpad
            }
          ),
          (recognizer) {
            recognizer
              ..onStart = (details) {
                debugPrint('onStart');
              }
              ..onUpdate = (details) {
                debugPrint('onUpdate');
              }
              ..onEnd = (details) {
                debugPrint('onEnd');
              };
          },
        ),
      },
      child: Container()
    );
  }
}

Using GestureRecognizer and Listener

After upgrading to Flutter 3.3.0, there won't be a change in behavior, as addPointerPanZoom must be called on each GestureRecognizer to allow it to track gestures. The following code won't receive pan gesture callbacks when the trackpad is scrolled:

void main() => runApp(Foo());

class Foo extends StatefulWidget {
  late final PanGestureRecognizer recognizer;

  @override
  void initState() {
    super.initState();
    recognizer = PanGestureRecognizer()
    ..onStart = _onPanStart
    ..onUpdate = _onPanUpdate
    ..onEnd = _onPanEnd;
  }

  void _onPanStart(DragStartDetails details) {
    debugPrint('onStart');
  }

  void _onPanUpdate(DragUpdateDetails details) {
    debugPrint('onUpdate');
  }

  void _onPanEnd(DragEndDetails details) {
    debugPrint('onEnd');
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerDown: recognizer.addPointer,
      // recognizer.addPointerPanZoom is not called
      child: Container()
    );
  }
}
Migration assistantAI-generated · grounded in cited sources
AI explanations are not configured
Set AI_PROVIDER (for example to openai-compatible with a local Ollama or LM Studio endpoint) to enable grounded explanations. Everything else on this page works without an AI provider.
Official migration guide·Verified classification (95%)

Listed in the official breaking-changes index (docs.flutter.dev/release/breaking-changes).

An official announcement or migration guide documents an intended behaviour or API change. A verified source change (git diff) shows code changed but does not by itself prove a public API is breaking. AI interpretations are labelled and never used to classify a change.

Source commit
b3045439b7c0
Source last updated
Oct 21, 2026
Last synchronized
Oct 21, 2026
Publication status
published
Publication history
  • Oct 21, 2026, 8:31 PM: new → published (ingestion) — Backed by an official breaking-change/migration guide.
AI explanationAI-generated · grounded in cited sources
AI explanations are not configured
Set AI_PROVIDER (for example to openai-compatible with a local Ollama or LM Studio endpoint) to enable grounded explanations. Everything else on this page works without an AI provider.
Was this useful?
Trackpad gestures can trigger GestureRecognizer · Boring Doc