overscroll_feedback 0.0.5 copy "overscroll_feedback: ^0.0.5" to clipboard
overscroll_feedback: ^0.0.5 copied to clipboard

Flutter overscroll feedback for any scrollable, with custom indicators, haptics, and reach-hold-release callbacks.

overscroll_feedback #

A Flutter widget that reveals custom feedback when users pull beyond a scroll edge.

It is useful for chat screens, message lists, feed pages, horizontal carousels, and any scrollable UI where you want to show a small "start reached" or "you reached the end" indicator, trigger haptic feedback, or run a callback when the user keeps pulling past the scroll limit.

Demo #

Overscroll Feedback demo

Features #

  • Reveal a custom widget when the user pulls beyond the end of a list.
  • Reveal feedback at the start, end, or both edges of the list.
  • Supports vertical and horizontal scrolling.
  • Release-after-hold detection for ListView, SingleChildScrollView, CustomScrollView, and other Flutter scrollables.
  • Works with simple string lists, custom item types, or lazy builders.
  • Progress-aware feedback builder for custom animations.
  • Optional haptic feedback when the pull distance reaches a threshold.
  • Custom haptic callback support.
  • One-shot callback when the scroll limit is reached.
  • Repeated hold callback while the user keeps pulling.
  • One-shot callback when the user releases after reaching the hold threshold.
  • Optional pull-to-load-more flow with loading, exhausted, and error feedback.
  • Configurable reveal distance, haptic trigger distance, feedback size, and offset.

Installation #

Add the package to your pubspec.yaml:

dependencies:
  overscroll_feedback: ^0.0.5

Then run:

flutter pub get

Import it:

import 'package:overscroll_feedback/overscroll_feedback.dart';

Which API should I use? #

Use the helper that matches your list:

API Best for
OverscrollFeedback.strings Quick demos, settings pages, or simple text lists.
OverscrollFeedback<T> Lists backed by your own model objects.
OverscrollFeedback.builder Large, paged, or dynamic lists where items should be built lazily.
ReachLimitListView Lower-level control when you want to provide every ListView option yourself.
ReachLimitListener Release-after-hold detection around any Flutter scrollable.

Quick start #

Use OverscrollFeedback.strings when you want a simple list of text items.

OverscrollFeedback.strings(
  messages: const [
    'Welcome aboard',
    'Pull gently',
    'Keep scrolling',
    'Now pull past the end',
  ],
  reachLimitWidget: DecoratedBox(
    decoration: BoxDecoration(
      color: Colors.blue,
      borderRadius: BorderRadius.circular(999),
    ),
    child: const Padding(
      padding: EdgeInsets.symmetric(horizontal: 18, vertical: 12),
      child: Text(
        'You reached the end',
        style: TextStyle(color: Colors.white),
      ),
    ),
  ),
  onReachLimit: () {
    debugPrint('Reached the bottom');
  },
)

Start, end, or both edges #

By default, feedback appears when the user pulls beyond the end of the list. Use position when you want top/start feedback too.

OverscrollFeedback.strings(
  position: OverscrollFeedbackPosition.both,
  messages: const [
    'First message',
    'Second message',
    'Third message',
  ],
  startReachLimitWidget: const Text('Start of list'),
  reachLimitWidget: const Text('End of list'),
)

For a top-only indicator:

OverscrollFeedback.strings(
  position: OverscrollFeedbackPosition.start,
  messages: const ['One', 'Two', 'Three'],
  startReachLimitWidget: const Text('Nothing above this'),
)

Custom item type #

Use the generic constructor when your list contains custom model objects.

class Message {
  const Message({
    required this.text,
    required this.sender,
  });

  final String text;
  final String sender;
}

final messages = [
  const Message(text: 'Hello', sender: 'Me'),
  const Message(text: 'Hi there', sender: 'Friend'),
];

OverscrollFeedback<Message>(
  items: messages,
  itemBuilder: (context, message, index) {
    return Container(
      margin: const EdgeInsets.only(bottom: 12),
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.grey.shade200,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Text('${message.sender}: ${message.text}'),
    );
  },
  reachLimitWidget: Container(
    padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
    decoration: BoxDecoration(
      color: Colors.black,
      borderRadius: BorderRadius.circular(999),
    ),
    child: const Text(
      'No more messages',
      style: TextStyle(color: Colors.white),
    ),
  ),
)

Lazy builder #

Use OverscrollFeedback.builder for large or dynamic lists. It works like Flutter's ListView.builder, so items are created only when needed.

OverscrollFeedback.builder(
  itemCount: 1000,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text('Message $index'),
    );
  },
  reachLimitWidget: const Text('No more messages'),
)

Pull to load more #

Provide onLoadMore to start an asynchronous request when the user pulls past the end threshold and releases. The widget prevents another request from starting until that future completes. Your application still owns the items, cursor or page number, and hasMore state.

OverscrollFeedback.builder(
  itemCount: items.length,
  itemBuilder: (context, index) => ListTile(
    title: Text(items[index].title),
  ),
  reachLimitWidget: const Text('Pull to load more'),
  hasMore: hasMore,
  onLoadMore: () async {
    final nextPage = await repository.loadNextPage();
    setState(() {
      items.addAll(nextPage.items);
      hasMore = nextPage.hasMore;
    });
  },
  loadingBuilder: (context, progress) {
    return const Center(child: CircularProgressIndicator());
  },
  noMoreBuilder: (context, progress) {
    return const Center(child: Text('Everything is loaded'));
  },
  loadMoreErrorBuilder: (context, error, progress) {
    return Center(child: Text('Load failed: $error. Pull to retry.'));
  },
)

If loading is also controlled outside this widget, pass isLoadingMore: true to show the loading feedback and block onLoadMore. When builders are omitted, the package provides simple loading, exhausted, and error indicators.

Animated feedback #

Use reachLimitBuilder when your feedback should react to pull progress. progress goes from 0.0 to 1.0.

OverscrollFeedback.builder(
  itemCount: 30,
  itemBuilder: (context, index) {
    return ListTile(title: Text('Item $index'));
  },
  reachLimitBuilder: (context, progress) {
    return Transform.scale(
      scale: 0.8 + (progress * 0.2),
      child: Opacity(
        opacity: progress,
        child: const Text('You reached the end'),
      ),
    );
  },
)

You can also animate the start indicator separately:

OverscrollFeedback.builder(
  position: OverscrollFeedbackPosition.both,
  itemCount: 30,
  itemBuilder: (context, index) {
    return ListTile(title: Text('Item $index'));
  },
  startReachLimitBuilder: (context, progress) {
    return Opacity(
      opacity: progress,
      child: const Text('Start reached'),
    );
  },
  reachLimitBuilder: (context, progress) {
    return Transform.scale(
      scale: 0.8 + (progress * 0.2),
      child: const Text('End reached'),
    );
  },
)

Horizontal list #

For horizontal lists, set isHorizontal: true.

A horizontal list needs a fixed height, so height is required when isHorizontal is true.

OverscrollFeedback.strings(
  isHorizontal: true,
  height: 140,
  messages: const [
    'Card',
    'Article',
    'Story',
    'Preview',
  ],
  itemWidth: 180,
  reachLimitWidget: Container(
    alignment: Alignment.center,
    padding: const EdgeInsets.symmetric(horizontal: 16),
    decoration: BoxDecoration(
      color: Colors.deepPurple,
      borderRadius: BorderRadius.circular(16),
    ),
    child: const Text(
      'End',
      style: TextStyle(color: Colors.white),
    ),
  ),
)

Callbacks #

onReachLimit #

Called once when the user pulls far enough to reach the feedback trigger.

OverscrollFeedback.strings(
  messages: const ['One', 'Two', 'Three'],
  reachLimitWidget: const Text('End reached'),
  onReachLimit: () {
    debugPrint('User reached the scroll limit');
  },
)

onReachLimitHold #

Called repeatedly while the user keeps holding/pulling beyond the limit.

OverscrollFeedback.strings(
  messages: const ['One', 'Two', 'Three'],
  reachLimitWidget: const Text('Keep holding...'),
  holdTriggerDistance: 140,
  holdInterval: const Duration(milliseconds: 500),
  onReachLimitHold: (pullDistance) {
    debugPrint('Still pulling: $pullDistance');
  },
)

onReachLimitHoldRelease #

Called once when the pointer is released, but only if the pull reached holdTriggerDistance during that gesture. Merely scrolling to the edge or releasing below the threshold does not call it.

OverscrollFeedback.strings(
  messages: const ['One', 'Two', 'Three'],
  reachLimitWidget: const Text('Release now'),
  holdTriggerDistance: 140,
  onReachLimitHoldRelease: (pullDistance) {
    debugPrint('Released after reaching: $pullDistance');
  },
)

Use with any scrollable #

Wrap a SingleChildScrollView, CustomScrollView, or another widget that emits Flutter ScrollNotifications with ReachLimitListener. The callback uses the same reach-then-release behavior as OverscrollFeedback.

ReachLimitListener(
  position: OverscrollFeedbackPosition.end,
  holdTriggerDistance: 120,
  onReachLimitHoldRelease: (pullDistance) {
    debugPrint('Released at the end: $pullDistance');
  },
  child: CustomScrollView(
    physics: const BouncingScrollPhysics(
      parent: AlwaysScrollableScrollPhysics(),
    ),
    slivers: [
      SliverList.builder(
        itemCount: 20,
        itemBuilder: (context, index) => ListTile(
          title: Text('Item $index'),
        ),
      ),
    ],
  ),
)

ReachLimitListener observes the nearest scrollable by default. Set scrollDirection for horizontal content and position for start, end, or both edges. The wrapped scrollable must use physics that permit overscroll; combining BouncingScrollPhysics with AlwaysScrollableScrollPhysics also supports content shorter than the viewport.

Custom haptic feedback #

By default, the widget uses Flutter haptic vibration when the haptic trigger distance is reached.

You can provide your own haptic feedback callback:

OverscrollFeedback.strings(
  messages: const ['One', 'Two', 'Three'],
  reachLimitWidget: const Text('Limit reached'),
  hapticFeedback: (pullDistance) {
    if (pullDistance > 180) {
      return HapticFeedback.heavyImpact();
    }

    return HapticFeedback.selectionClick();
  },
)

You can also disable haptics:

OverscrollFeedback.strings(
  messages: const ['One', 'Two', 'Three'],
  reachLimitWidget: const Text('Limit reached'),
  hapticsEnabled: false,
)

Common customization #

These options are useful for tuning the feel of the interaction:

Option What it controls
position Which edge can reveal feedback: start, end, or both.
reachLimitExtent The space reserved for the revealed feedback widget.
offset How far the feedback slides while it is being revealed.
revealStartDistance Pull distance before feedback starts appearing.
revealEndDistance Pull distance where feedback reaches full opacity.
hapticTriggerDistance Pull distance where haptics and onReachLimit trigger.
holdTriggerDistance Pull distance where repeated hold callbacks begin.
holdInterval How often onReachLimitHold runs while the user keeps pulling.
onReachLimitHoldRelease Runs once on release after the hold threshold was reached.

Notes #

This package listens to Flutter scroll notifications. For the best effect on iOS-style bouncing scroll, keep the default BouncingScrollPhysics. On platforms that normally clamp overscroll, the package uses AlwaysScrollableScrollPhysics by default so short lists can still be pulled. When using ReachLimitListener, configure the physics on its child scrollable.

Tuning the reveal behavior #

You can control when the feedback starts appearing, when it becomes fully visible, and when haptics/callbacks are triggered.

OverscrollFeedback.strings(
  messages: const ['One', 'Two', 'Three'],
  reachLimitWidget: const Text('You reached the end'),
  revealStartDistance: 32,
  revealEndDistance: 120,
  hapticTriggerDistance: 140,
)

Important values #

Property Description
revealStartDistance Pull distance where the feedback widget starts appearing.
revealEndDistance Pull distance where the feedback widget becomes fully visible.
hapticTriggerDistance Pull distance where haptic feedback and onReachLimit trigger.
holdTriggerDistance Pull distance where the repeated hold callback starts.
holdInterval How often onReachLimitHold is called while holding.
onReachLimitHoldRelease Called once upon release after reaching the hold threshold.
reachLimitExtent Space reserved for the revealed feedback widget.
offset Starting offset animation for the revealed widget.
hapticsEnabled Enables or disables built-in haptic feedback.
hapticFeedback Custom haptic callback.

Full example #

import 'package:flutter/material.dart';
import 'package:overscroll_feedback/overscroll_feedback.dart';

class OverscrollDemoPage extends StatelessWidget {
  const OverscrollDemoPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: OverscrollFeedback.strings(
          messages: List.generate(12, (index) => 'Message'),
          revealStartDistance: 40,
          revealEndDistance: 120,
          hapticTriggerDistance: 135,
          reachLimitWidget: Container(
            alignment: Alignment.center,
            padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
            decoration: BoxDecoration(
              color: Colors.black,
              borderRadius: BorderRadius.circular(999),
            ),
            child: const Text(
              'You reached the end',
              style: TextStyle(color: Colors.white),
            ),
          ),
          onReachLimit: () {
            debugPrint('Reached the list end');
          },
          onReachLimitHold: (pullDistance) {
            debugPrint('Holding at distance: $pullDistance');
          },
          onReachLimitHoldRelease: (pullDistance) {
            debugPrint('Released after holding: $pullDistance');
          },
        ),
      ),
    );
  }
}

Notes #

Haptics #

Haptic feedback depends on the device, platform, and system settings.

For best testing, use a real device instead of a simulator or emulator. Some devices may ignore repeated vibration requests or make them feel weaker than expected.

Android vibration permission #

If your custom haptic implementation uses a vibration plugin, you may need to add the Android vibration permission:

<uses-permission android:name="android.permission.VIBRATE"/>

Flutter's built-in HapticFeedback does not require extra Dart setup.

Horizontal lists #

When using isHorizontal: true, provide a fixed height.

OverscrollFeedback.strings(
  isHorizontal: true,
  height: 160,
  messages: const ['One', 'Two', 'Three'],
  reachLimitWidget: const Text('End'),
)

Example app #

A runnable example app is included in the example folder.

cd example
flutter run

API overview #

OverscrollFeedback<T> #

Generic widget for custom item types.

OverscrollFeedback<T>({
  required List<T> items,
  required ReachLimitItemBuilder<T> itemBuilder,
  required Widget reachLimitWidget,
})

OverscrollFeedback.strings #

Convenience constructor for simple string lists.

OverscrollFeedback.strings({
  required List<String> messages,
  required Widget reachLimitWidget,
})

ReachLimitListener #

Scrollable-agnostic reach-and-release listener.

ReachLimitListener({
  required Widget child,
  required ReachLimitHoldReleaseCallback onReachLimitHoldRelease,
  double holdTriggerDistance = 138,
  OverscrollFeedbackPosition position = OverscrollFeedbackPosition.end,
  Axis scrollDirection = Axis.vertical,
})

License #

MIT

3
likes
150
points
306
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Flutter overscroll feedback for any scrollable, with custom indicators, haptics, and reach-hold-release callbacks.

Repository (GitHub)
View/report issues

Topics

#flutter #scrollview #overscroll #scroll-effects #haptics

License

MIT (license)

Dependencies

flutter

More

Packages that depend on overscroll_feedback