Skip to content
Blog

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

  1. Use Listener for raw scroll events — it doesn’t interfere with gesture detection
  2. Clamp scroll bounds — prevent scrolling beyond content
  3. Add scroll indicators — show users they can scroll
  4. Respect platform conventions — natural scrolling on macOS, standard on Windows
  5. 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 RepaintBoundary to 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.