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)
| 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 | } |
| 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()
);
}
}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.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.
- Official migration guide
- GestureDetector
- RawGestureDetector
- GestureRecognizer
- Flutter Trackpad Gestures
- Issue 23604
- Support trackpad gestures in framework
- iPad trackpad gestures
- Linux trackpad gestures
- Mac trackpad gestures
- Win32 trackpad gestures
- ChromeOS/Android trackpad gestures
- 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_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.