Scroll Wheel for Custom Widgets in Flutter
Handle scroll wheel events in custom Flutter widgets. Build scrollable interfaces that respond to mouse wheels and trackpads on desktop and web.
Published on • September 17, 2026
AI Assistant

Beyond ScrollView
Most Flutter widgets handle scrolling automatically — ListView, GridView, SingleChildScrollView. But when you build custom widgets that need scroll behavior, you need to handle the scroll wheel directly.
Listener: The Basic Approach
Listener(
onPointerSignal: (event) {
if (event is PointerScrollEvent) {
setState(() {
_offset = (_offset + event.scrollDelta.dy).clamp(0.0, _maxScroll);
});
}
},
child: Transform.translate(
offset: Offset(0, -_offset),
child: MyCustomContent(),
),
)
NotificationListener for Scrollable Widgets
For widgets that participate in Flutter’s scroll system:
NotificationListener<ScrollNotification>(
onNotification: (notification) {
// Handle scroll notifications
return true; // Consumed
},
child: SingleChildScrollView(
child: MyContent(),
),
)
Custom Scrollable Widget
Build a reusable scrollable container:
class ScrollableBox extends StatefulWidget {
final Widget child;
final double maxScroll;
const ScrollableBox({required this.child, required this.maxScroll});
@override
State<ScrollableBox> createState() => _ScrollableBoxState();
}
class _ScrollableBoxState extends State<ScrollableBox> {
double _scrollOffset = 0;
@override
Widget build(BuildContext context) {
return Listener(
onPointerSignal: (event) {
if (event is PointerScrollEvent) {
setState(() {
_scrollOffset = (_scrollOffset + event.scrollDelta.dy)
.clamp(0.0, widget.maxScroll);
});
}
},
child: ClipRect(
child: Transform.translate(
offset: Offset(0, -_scrollOffset),
child: widget.child,
),
),
);
}
}
Horizontal Scroll
Handle horizontal scrolling:
Listener(
onPointerSignal: (event) {
if (event is PointerScrollEvent) {
// Horizontal scroll with Shift+scroll
final isHorizontal = HardwareKeyboard.instance.isShiftPressed;
setState(() {
if (isHorizontal) {
_offsetX = (_offsetX + event.scrollDelta.dy).clamp(0.0, maxX);
} else {
_offsetY = (_offsetY + event.scrollDelta.dy).clamp(0.0, maxY);
}
});
}
},
child: MyContent(),
)
Smooth Scrolling with Animation
Add momentum and animation:
class AnimatedScroller extends StatefulWidget {
@override
State<AnimatedScroller> createState() => _AnimatedScrollerState();
}
class _AnimatedScrollerState extends State<AnimatedScroller>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
double _targetOffset = 0;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: Duration(milliseconds: 300));
}
void _onScroll(double delta) {
_targetOffset = (_targetOffset + delta).clamp(0.0, 1000.0);
_controller.forward(from: 0);
// Animate to _targetOffset
}
}
Best Practices
- Use
Listenerfor raw scroll events — it doesn’t interfere with gesture detection - Clamp scroll bounds — prevent scrolling beyond content
- Add scroll indicators — show users they can scroll
- Respect platform conventions — natural scrolling on macOS, standard on Windows
- Test with different devices — mouse wheel, trackpad, and scroll ball
Performance
Scroll handlers run on every pointer signal. Keep them efficient:
- Avoid expensive calculations in
onPointerSignal - Use
RepaintBoundaryto isolate scroll regions - Debounce rapid scroll events if needed
Conclusion
Custom scroll handling gives you full control over how your Flutter widgets respond to mouse wheels and trackpads. Combined with animation and proper bounds, it creates smooth, professional scrollable experiences on desktop and web.