Custom Scroll Physics in Flutter

FlutterDartUIAnimation
Share on LinkedIn

ScrollPhysics is the Flutter class that decides how scrolling feels — the drag friction, whether the edge bounces or clamps, how far a fling coasts, and whether the list settles onto specific offsets. It's a separate concern from what you scroll (slivers, lists) and how items lay out; physics only shapes the motion. Most apps never touch it, and most apps are right not to. But when you need snap-to-item paging, a carousel that locks onto cards, or scroll behavior that matches a specific platform, subclassing ScrollPhysics is the correct, surprisingly small amount of code.

I've watched teams reinvent snapping with NotificationListener hacks and post-fling animateTo calls that fight the user's finger and stutter. Doing it through physics instead means the snapping is part of the momentum simulation, so it feels native because it literally uses the same machinery.

The built-in physics you already have

Before subclassing, know the stock options, because composition covers a lot:

These compose via applyTo. AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics()) means "always scrollable, and bounce at the edges." Physics chain like decorators, so you rarely start from scratch.

How physics actually works

Two overrides carry most of the behavior:

The ScrollMetrics passed to these methods tell you everything about the current state: pixels (current offset), minScrollExtent, maxScrollExtent, viewportDimension. That's enough to compute where you want to land.

Snap-to-item physics

Here's the pattern for a carousel that locks onto fixed-width cards. The idea: at the end of a fling, compute the nearest item boundary and return a spring simulation toward it.

class SnapScrollPhysics extends ScrollPhysics {
  const SnapScrollPhysics({required this.itemExtent, super.parent});

  final double itemExtent;

  @override
  SnapScrollPhysics applyTo(ScrollPhysics? ancestor) =>
      SnapScrollPhysics(itemExtent: itemExtent, parent: buildParent(ancestor));

  double _target(ScrollMetrics position, double velocity) {
    // Where a normal fling would end...
    final base = position.pixels + velocity * 0.15;
    // ...snapped to the nearest item boundary, clamped to range.
    final index = (base / itemExtent).round();
    return (index * itemExtent)
        .clamp(position.minScrollExtent, position.maxScrollExtent);
  }

  @override
  Simulation? createBallisticSimulation(
      ScrollMetrics position, double velocity) {
    final target = _target(position, velocity);
    if ((target - position.pixels).abs() < precisionErrorTolerance) {
      return null;
    }
    return ScrollSpringSimulation(
      spring, position.pixels, target, velocity,
      tolerance: toleranceFor(position),
    );
  }

  @override
  bool get allowImplicitScrolling => false;
}

Attach it with ListView(physics: const SnapScrollPhysics(itemExtent: 320), ...). Because the snap is expressed as the destination of the ballistic simulation, the settle uses the same spring the framework uses everywhere — no post-hoc animateTo, no fighting the gesture, no visible correction jump.

Matching platform feel deliberately

Flutter's ScrollBehavior picks bouncing on iOS and clamping on Android by default, which is usually what you want. But there are legitimate reasons to override globally — a design system that mandates one feel everywhere, or an embedded webview-like surface. Do it at the ScrollConfiguration level rather than sprinkling physics: on every list:

class AppScrollBehavior extends MaterialScrollBehavior {
  @override
  ScrollPhysics getScrollPhysics(BuildContext context) =>
      const BouncingScrollPhysics();
}

Wrap your MaterialApp with a scrollBehavior: AppScrollBehavior() and every scrollable inherits it. Centralizing this beats hunting down individual lists later.

Where I stop and use PageView

If you want full-viewport paging, don't write custom physics — PageView with PageScrollPhysics already nails it, including the resistance and snap. Reach for custom ScrollPhysics only when the snap targets aren't full pages (fixed-width cards in a horizontal list, sticky sections, magnetic scroll to headers). Knowing when not to subclass is half the skill.

Debugging the feel

Scroll feel is subjective, so measure. Enable the performance overlay (showPerformanceOverlay: true) and watch for jank during flings; a stuttering snap usually means you're doing work in a scroll listener instead of in the simulation. Test on a real low-end Android device, not just the simulator — the iOS simulator in particular lies about scroll smoothness. And test with both a slow drag-release and a hard flick, because the velocity term is where snap logic most often gets the target wrong.

Custom physics pairs naturally with custom scroll structure; if you're building elaborate scroll UIs, the layout side lives in slivers and CustomScrollView, and truly bespoke scrolling widgets sometimes bottom out in a RenderObject.

What I'd take away

Reach for the composable built-ins first — bouncing, clamping, always-scrollable, page — and only subclass ScrollPhysics when you need snap targets the framework doesn't provide. Put snapping logic in createBallisticSimulation by returning a spring toward the computed target, so the settle uses the same simulation machinery as native scrolling and feels right. Centralize platform feel in a ScrollBehavior, and always validate on a real low-end device. Get it right and users never notice the physics — which is exactly the point.

Resources

Frequently asked questions

What does ScrollPhysics control in Flutter?

ScrollPhysics determines how a scroll view responds to user input and momentum — the friction while dragging, whether it overscrolls with a bounce or a glow, how far a fling carries, and whether it settles onto specific offsets. It does not control what is scrolled or how items are laid out; it only shapes the feel of the motion. You swap it via the physics parameter on any scrollable.

How do I make a list snap to items in Flutter?

Subclass ScrollPhysics and override createBallisticSimulation to return a simulation that targets the nearest item boundary at the end of a fling or drag. PageScrollPhysics already does this for full-viewport pages, so for full-page snapping you just set physics: PageScrollPhysics(). For custom item sizes you compute the nearest multiple of your item extent and animate to it.

What is the difference between BouncingScrollPhysics and ClampingScrollPhysics?

BouncingScrollPhysics is the iOS-style behavior where content overscrolls past the edge and springs back. ClampingScrollPhysics is the Android-style behavior where the content stops hard at the edge and shows a stretch or glow indicator. By default Flutter picks per platform, but you can force either, or compose them onto AlwaysScrollableScrollPhysics to keep scrolling enabled even when content fits.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →