Drag, Pinch & Swipe

Gestures covers NibMotion’s while* props and drag prop — gesture-reactive behavior layered onto an animation you’re already declaring with NibMotion. This page covers a different set of tools: NibDraggable, NibPinch, and NibSwipeDismiss are standalone, drop-in widgets that handle a single gesture interaction end-to-end. None of them require wrapping anything in NibMotion — each one owns its own GestureDetector and its own internal MotionValue, and you just wrap your child widget directly. Reach for these when you want “make this draggable” / “make this pinch-to-zoom” / “make this swipe-to-dismiss” as a single, self-contained widget, rather than as one of several props on a larger NibMotion animation.

All three follow the same internal pattern as the rest of nib_motion: a MotionValue is written directly from gesture callbacks and read directly by a RenderProxyBox during paint, so dragging, pinching, and swiping never trigger a widget rebuild.

NibDraggable

NibDraggable wraps a child in free or axis-locked dragging, with optional bounds and an optional spring-back to its origin (or a custom snapTarget) on release.

import 'package:flutter/material.dart';
import 'package:nib_motion/nib_motion.dart';
 
class DraggableChip extends StatelessWidget {
  const DraggableChip({super.key});
 
  @override
  Widget build(BuildContext context) {
    return NibDraggable(
      bounds: const Rect.fromLTRB(-100, -100, 100, 100),
      snapBack: true,
      onDragEnd: (finalOffset, velocity) {
        debugPrint('released at $finalOffset, velocity $velocity');
      },
      child: Container(
        width: 64,
        height: 64,
        decoration: BoxDecoration(
          color: Theme.of(context).colorScheme.primary,
          borderRadius: BorderRadius.circular(16),
        ),
      ),
    );
  }
}

With snapBack: true, releasing the chip animates it back to Offset.zero (or snapTarget, if set) using spring — the default is NibSpringDescription.gentle, the same spring presets used throughout nib_motion. Set axis to lock dragging to Axis.horizontal or Axis.vertical, just like NibDragConfig.axis in the NibMotion.drag system — NibDraggable is a simpler, focused alternative for the common case, not a replacement for it.

NibPinch

NibPinch wraps a child in pinch-to-zoom: a uniform scale driven by GestureDetector.onScaleUpdate, clamped to minScale/maxScale, with an optional spring snap-back to initialScale on release.

import 'package:flutter/material.dart';
import 'package:nib_motion/nib_motion.dart';
 
class ZoomableImage extends StatelessWidget {
  const ZoomableImage({super.key, required this.imageUrl});
 
  final String imageUrl;
 
  @override
  Widget build(BuildContext context) {
    return NibPinch(
      minScale: 1.0,
      maxScale: 3.0,
      snapBackOnRelease: true,
      onScaleChanged: (scale) => debugPrint('scale: $scale'),
      child: ClipRRect(
        borderRadius: BorderRadius.circular(12),
        child: Image.network(imageUrl, fit: BoxFit.cover),
      ),
    );
  }
}

snapBackOnRelease: true is what makes this feel like a typical photo-viewer pinch — release at any zoom level and the image eases back to initialScale (default 1.0) rather than staying zoomed in. Leave it false if you want the zoom level to persist across gestures.

NibSwipeDismiss

NibSwipeDismiss wraps a child in swipe-to-dismiss: drag it past dismissThreshold and releasing animates it the rest of the way off-screen before calling onDismissed; release before the threshold and it springs back to its resting position.

Because package:flutter/material.dart already exports its own DismissDirection (used by Flutter’s own Dismissible widget), NibSwipeDismiss’s DismissDirection enum collides with it on import. Hide Flutter’s version when importing Material alongside nib_motion:

import 'package:flutter/material.dart' hide DismissDirection;
import 'package:nib_motion/nib_motion.dart';
 
class DismissibleListItem extends StatelessWidget {
  const DismissibleListItem({
    super.key,
    required this.title,
    required this.onDismissed,
  });
 
  final String title;
  final VoidCallback onDismissed;
 
  @override
  Widget build(BuildContext context) {
    return NibSwipeDismiss(
      dismissDirection: DismissDirection.endToStart,
      dismissThreshold: 0.4,
      onDismissed: onDismissed,
      background: Container(
        color: Theme.of(context).colorScheme.errorContainer,
        alignment: Alignment.centerRight,
        padding: const EdgeInsets.symmetric(horizontal: 20),
        child: Icon(Icons.delete, color: Theme.of(context).colorScheme.error),
      ),
      child: Container(
        color: Theme.of(context).colorScheme.surface,
        padding: const EdgeInsets.all(16),
        child: Text(title),
      ),
    );
  }
}

background is shown behind child in a Stack only while the swipe is in progress — the classic “reveal a delete icon as you swipe” pattern. dismissDirection: DismissDirection.endToStart means only a right-to-left swipe counts toward dismissal; a swipe the other way just springs back regardless of how far it travels. Use DismissDirection.horizontal or .vertical to accept either direction along that axis, or .startToEnd for the mirror image of .endToStart.

API reference

NibDraggable

PropTypeDefaultDescription
childWidgetrequiredThe widget to make draggable.
axisAxis?nullLocks dragging to a single axis. null allows free dragging.
boundsRect?nullThe bounding box the drag offset is confined to, relative to the widget’s resting position. null means unconstrained.
snapBackboolfalseWhether to spring back to the origin (or snapTarget) when the drag gesture ends.
snapTargetOffset?nullIf set, the offset snapBack animates to on release instead of Offset.zero.
onDragStartVoidCallback?nullCalled when a drag gesture begins.
onDragUpdateValueChanged<Offset>?nullCalled with the current drag offset on every pan update.
onDragEndvoid Function(Offset finalOffset, Offset velocity)?nullCalled when a drag gesture ends, with the final offset and the release velocity.
springNibSpringDescriptionNibSpringDescription.gentleThe spring physics used to animate back to the origin or snapTarget when snapBack is true.

NibPinch

PropTypeDefaultDescription
childWidgetrequiredThe widget to make pinchable.
minScaledouble0.5The smallest scale factor the gesture can reach.
maxScaledouble4.0The largest scale factor the gesture can reach.
initialScaledouble1.0The scale child starts at, and the target snapBackOnRelease springs back to.
snapBackOnReleaseboolfalseWhether to spring back to initialScale when the gesture ends.
onScaleChangedValueChanged<double>?nullCalled with the current scale on every scale update.
springNibSpringDescriptionNibSpringDescription.gentleThe spring physics used to animate back to initialScale when snapBackOnRelease is true.

NibSwipeDismiss

PropTypeDefaultDescription
childWidgetrequiredThe widget to make dismissible.
onDismissedVoidCallbackrequiredCalled once the widget has finished animating off-screen past dismissThreshold.
dismissThresholddouble0.5The fraction of the widget’s extent (width for horizontal directions, height for vertical) the swipe must cross before release triggers a dismissal instead of a spring-back.
dismissDirectionDismissDirectionDismissDirection.horizontalWhich direction(s) of swipe are recognized.
backgroundWidget?nullA widget shown behind child (via a Stack) while swiping.
springNibSpringDescriptionNibSpringDescription.gentleThe spring physics used to animate back to 0 when the swipe doesn’t cross dismissThreshold.

DismissDirection

ValueDescription
horizontalSwiping left or right both trigger dismissal.
verticalSwiping up or down both trigger dismissal.
startToEndOnly swiping left-to-right (i.e. a positive horizontal offset) triggers dismissal.
endToStartOnly swiping right-to-left (i.e. a negative horizontal offset) triggers dismissal.

Note that this DismissDirection is exported by nib_motion, not package:flutter/material.dart — if you import both, hide Material’s version with import 'package:flutter/material.dart' hide DismissDirection; as shown above.

Next steps

  • Gestures — for gesture-reactive behavior layered onto a NibMotion you’re already animating with while* props or the drag prop, rather than a standalone widget.
  • Animate — combine these gesture widgets with NibMotion by wrapping one around the other, e.g. animating opacity or color changes in response to onDragUpdate/onScaleChanged.
  • PresenceNibSwipeDismiss’s onDismissed pairs naturally with removing an item from a list inside a NibPresence, so the rest of the list animates smoothly into the gap.