Add gestures to your FlutterFlow app by attaching action flows to any widget's On Tap, On Long Press, and On Double Tap events in the Properties Panel. For advanced gestures like pinch-to-zoom, drag-and-drop, and swipe-to-dismiss, use Custom Widgets backed by Flutter's GestureDetector, InteractiveViewer, Draggable, and DragTarget. Never nest conflicting GestureDetectors on the same axis — pick a winner with the gesture arena or separate them visually.
The Complete Gesture System in FlutterFlow
Gestures are what turn a static app into a fluid, intuitive experience. FlutterFlow supports the most common gestures natively through the Properties Panel's Action section — tap, long press, and double tap are available on almost every widget without any code. More advanced gestures like pinch-to-zoom, drag-and-drop, and horizontal swipe require Custom Widgets that wrap Flutter's built-in gesture handling widgets. The key concept to understand is the gesture arena: when multiple widgets are listening for the same gesture type on the same area of the screen, Flutter's gesture system holds an auction to decide which widget wins. Misconfiguring nested gesture recognizers is the single most common source of bugs in gesture-heavy FlutterFlow apps.
Prerequisites
- A FlutterFlow project with at least one interactive page
- Basic understanding of FlutterFlow's Properties Panel and Action Flows
- FlutterFlow Pro plan for Custom Widgets (required for pinch-to-zoom and drag-and-drop)
Step-by-step guide
Attach tap, long press, and double tap via the Properties Panel
Attach tap, long press, and double tap via the Properties Panel
In FlutterFlow, any widget can respond to tap, long press, and double tap without code. Select a widget on the canvas and look at the right-hand Properties Panel. Scroll down to the Actions section. Click the On Tap '+' button to add an action flow for taps — you can navigate to a page, show a bottom sheet, update App State, or call an API. Click On Long Press to add actions for press-and-hold (useful for context menus, quick actions, or triggering selection mode in lists). Click On Double Tap for actions like toggling a like/favourite on a post. All three can coexist on the same widget — they are distinguished by timing. For a smooth feel, add a visual feedback effect: in the widget's properties, enable Tap Effect (available on Container and Card widgets) which shows a brief scale or colour change on tap.
Expected result: The widget responds visually and triggers the correct action flow on tap, long press, and double tap as separate distinct interactions.
Implement pinch-to-zoom with InteractiveViewer
Implement pinch-to-zoom with InteractiveViewer
FlutterFlow has no built-in pinch-to-zoom — use a Custom Widget. Create a widget named PinchZoomViewer that accepts a child widget or an imageUrl parameter. Wrap the content in Flutter's InteractiveViewer widget. Set minScale to 0.5 and maxScale to 4.0. Set boundaryMargin to EdgeInsets.all(double.infinity) to allow panning beyond the widget's visible bounds. Set clipBehavior to Clip.none if you want the zoomed content to extend beyond the container. For image zoom specifically, use CachedNetworkImage inside the InteractiveViewer. Add a double-tap GestureDetector outside the InteractiveViewer to reset zoom to 1.0 using a TransformationController when the user double-taps.
1// Custom Widget: PinchZoomViewer2import 'package:flutter/material.dart';34class PinchZoomViewer extends StatefulWidget {5 final Widget child;6 const PinchZoomViewer({Key? key, required this.child}) : super(key: key);78 @override9 State<PinchZoomViewer> createState() => _PinchZoomViewerState();10}1112class _PinchZoomViewerState extends State<PinchZoomViewer>13 with SingleTickerProviderStateMixin {14 final _transformController = TransformationController();15 late AnimationController _animController;16 Animation<Matrix4>? _animation;1718 @override19 void initState() {20 super.initState();21 _animController = AnimationController(22 vsync: this, duration: const Duration(milliseconds: 300));23 }2425 @override26 void dispose() {27 _transformController.dispose();28 _animController.dispose();29 super.dispose();30 }3132 void _resetZoom() {33 _animation = Matrix4Tween(34 begin: _transformController.value,35 end: Matrix4.identity(),36 ).animate(CurvedAnimation(parent: _animController, curve: Curves.easeOut));37 _animation!.addListener(() => _transformController.value = _animation!.value);38 _animController.forward(from: 0);39 }4041 @override42 Widget build(BuildContext context) {43 return GestureDetector(44 onDoubleTap: _resetZoom,45 child: InteractiveViewer(46 transformationController: _transformController,47 minScale: 0.5,48 maxScale: 4.0,49 child: widget.child,50 ),51 );52 }53}Expected result: Users can pinch to zoom in to 4x, pan around the zoomed content, and double-tap to animate back to the original scale.
Build drag and drop with Draggable and DragTarget
Build drag and drop with Draggable and DragTarget
Drag-and-drop in FlutterFlow requires a Custom Widget combining Flutter's Draggable and DragTarget. Draggable wraps the item being dragged — set feedback to the visual that follows the finger (usually a semi-transparent copy of the item), childWhenDragging to a placeholder (dimmed or empty), and data to the item identifier. DragTarget wraps the drop zone — set onAccept to trigger when a valid Draggable is dropped onto it, and builder to change the visual appearance when a Draggable is hovering over it (highlight the drop zone). For a Kanban board or sortable list, store the item positions in a Page State list and update the list in the DragTarget's onAccept callback.
1// Custom Widget: DraggableCard2// Accepts an onDropped callback with the itemId and targetZone3import 'package:flutter/material.dart';45class DraggableCard extends StatelessWidget {6 final String itemId;7 final String label;8 final VoidCallback? onLongPressDrag;910 const DraggableCard({11 Key? key,12 required this.itemId,13 required this.label,14 this.onLongPressDrag,15 }) : super(key: key);1617 @override18 Widget build(BuildContext context) {19 return LongPressDraggable<String>(20 data: itemId,21 // Show while dragging (ghost)22 feedback: Material(23 elevation: 8,24 child: Container(25 padding: const EdgeInsets.all(12),26 color: Colors.blue.withOpacity(0.8),27 child: Text(label, style: const TextStyle(color: Colors.white)),28 ),29 ),30 // Show in original position while dragging31 childWhenDragging: Opacity(32 opacity: 0.3,33 child: _cardContent(),34 ),35 child: _cardContent(),36 );37 }3839 Widget _cardContent() {40 return Container(41 padding: const EdgeInsets.all(12),42 decoration: BoxDecoration(43 color: Colors.blue,44 borderRadius: BorderRadius.circular(8),45 ),46 child: Text(label, style: const TextStyle(color: Colors.white)),47 );48 }49}Expected result: Items can be long-pressed to initiate a drag. A semi-transparent ghost follows the finger. Dropping on a DragTarget zone triggers the item rearrangement.
Add swipe gestures for list items
Add swipe gestures for list items
For swipe-to-dismiss or swipe-to-reveal actions on list items, use Flutter's built-in Dismissible widget inside a Custom Widget. Wrap each list item in a Dismissible widget. Set key to a ValueKey(itemId) — each Dismissible must have a unique key. Set background to a red Container with a delete icon (shown during left swipe). Set secondaryBackground for right-swipe actions (e.g., archive). In the confirmDismiss callback, show a dialog asking 'Are you sure?' and return the user's decision as a Boolean. In the onDismissed callback, delete the Firestore document and call setState to remove the item from the list. For swipe-to-reveal (showing action buttons without full dismissal), use the flutter_slidable package via a Custom Widget.
1// Custom Widget: SwipeToDismissItem2import 'package:flutter/material.dart';34class SwipeToDismissItem extends StatelessWidget {5 final String itemId;6 final Widget child;7 final Future<bool?> Function(DismissDirection) onConfirmDismiss;8 final void Function(DismissDirection) onDismissed;910 const SwipeToDismissItem({11 Key? key,12 required this.itemId,13 required this.child,14 required this.onConfirmDismiss,15 required this.onDismissed,16 }) : super(key: key);1718 @override19 Widget build(BuildContext context) {20 return Dismissible(21 key: ValueKey(itemId),22 direction: DismissDirection.endToStart,23 confirmDismiss: onConfirmDismiss,24 onDismissed: onDismissed,25 background: Container(26 color: Colors.red,27 alignment: Alignment.centerRight,28 padding: const EdgeInsets.symmetric(horizontal: 20),29 child: const Icon(Icons.delete, color: Colors.white),30 ),31 child: child,32 );33 }34}Expected result: Swiping left on a list item reveals a red delete background. On confirmed swipe, the item animates out and the Firestore document is deleted.
Resolve gesture conflicts between nested GestureDetectors
Resolve gesture conflicts between nested GestureDetectors
Gesture conflicts occur when a parent widget and a child widget both listen for the same gesture type. For example, a horizontal swipe on a PageView and a horizontal scroll on a ListView inside the PageView will conflict. Flutter's gesture arena resolves conflicts by having all competing gesture recognizers race — the first one to claim the gesture wins. To resolve conflicts explicitly: use the HitTestBehavior.opaque parameter on GestureDetector to absorb all touches and prevent parent detection. Use IgnorePointer to completely block a subtree from receiving gestures. For vertical-vs-horizontal conflicts, use the onVerticalDragStart vs onHorizontalDragStart separation to distinguish directions at the DragGestureRecognizer level. Test gesture conflicts on physical devices — the simulator does not accurately reproduce multi-touch behavior.
Expected result: Each gesture type is handled by exactly one widget — no accidental page swipes while scrolling a list, and no accidental list scrolls while trying to swipe-to-dismiss.
Complete working example
1// Custom Widget: PinchZoomViewer2// Full implementation with double-tap reset animation3// Parameters: imageUrl (String) — optional, use child widget if empty4import 'package:flutter/material.dart';5import 'package:cached_network_image/cached_network_image.dart';67class PinchZoomViewer extends StatefulWidget {8 final String? imageUrl;9 final Widget? child;10 final double minScale;11 final double maxScale;1213 const PinchZoomViewer({14 Key? key,15 this.imageUrl,16 this.child,17 this.minScale = 0.5,18 this.maxScale = 4.0,19 }) : super(key: key);2021 @override22 State<PinchZoomViewer> createState() => _PinchZoomViewerState();23}2425class _PinchZoomViewerState extends State<PinchZoomViewer>26 with SingleTickerProviderStateMixin {27 final _controller = TransformationController();28 late AnimationController _animController;29 Animation<Matrix4>? _resetAnim;3031 @override32 void initState() {33 super.initState();34 _animController = AnimationController(35 vsync: this,36 duration: const Duration(milliseconds: 250),37 );38 }3940 @override41 void dispose() {42 _controller.dispose();43 _animController.dispose();44 super.dispose();45 }4647 void _onDoubleTap() {48 // If already at identity, zoom in; otherwise reset to identity49 final isZoomed = _controller.value != Matrix4.identity();50 final target = isZoomed ? Matrix4.identity() : (Matrix4.identity()..scale(2.0));5152 _resetAnim = Matrix4Tween(53 begin: _controller.value,54 end: target,55 ).animate(CurvedAnimation(parent: _animController, curve: Curves.easeInOut));56 _resetAnim!.addListener(() {57 if (mounted) _controller.value = _resetAnim!.value;58 });59 _animController.forward(from: 0);60 }6162 @override63 Widget build(BuildContext context) {64 final content = widget.imageUrl != null65 ? CachedNetworkImage(66 imageUrl: widget.imageUrl!,67 fit: BoxFit.contain,68 )69 : widget.child ?? const SizedBox.shrink();7071 return GestureDetector(72 onDoubleTap: _onDoubleTap,73 child: InteractiveViewer(74 transformationController: _controller,75 minScale: widget.minScale,76 maxScale: widget.maxScale,77 clipBehavior: Clip.none,78 child: content,79 ),80 );81 }82}Common mistakes
Why it's a problem: Nesting multiple GestureDetectors with conflicting gestures on the same axis
How to avoid: Separate gesture detection by axis: let the parent handle vertical scroll and the child handle horizontal swipe. Use DismissDirection.endToStart on Dismissible to only respond to a single direction. If both parent and child need the same direction, use a custom GestureRecognizer that explicitly releases the arena in favor of the correct widget.
Why it's a problem: Forgetting to add unique keys to Dismissible list items
How to avoid: Always set key: ValueKey(item.id) using the Firestore document ID or another stable unique identifier. Never use the list index as the key for Dismissible or ReorderableListView items.
Why it's a problem: Using GestureDetector on top of a widget that already handles taps (Button, InkWell)
How to avoid: Use the widget's built-in action hooks (OnTap in FlutterFlow Properties Panel, onPressed on buttons) instead of wrapping in GestureDetector. If you need custom gesture logic on a button, use GestureDetector alone without any button widget inside.
Best practices
- Use FlutterFlow's built-in On Tap, On Long Press, and On Double Tap for simple interactions — no code needed
- Always assign unique ValueKey(itemId) to Dismissible and ReorderableListView items
- Prefer LongPressDraggable over Draggable for list items to prevent accidental drag during scroll
- Test gesture behavior on a physical device — iOS simulator does not support multi-touch pinch gestures
- Separate gesture detection by axis (vertical vs horizontal) to prevent arena conflicts in nested scrollables
- Add visual feedback for every interactive gesture — use Tap Effect, InkWell ripple, or scale animation
- Document gesture behavior in comments when nesting GestureDetectors to clarify which widget wins each gesture type
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I am building a FlutterFlow app where users can reorder items in a list by dragging. I want to use Flutter's Draggable and DragTarget widgets. How do I implement a drag-to-reorder list where each item can be long-pressed to initiate a drag and dropped onto another position, updating a Dart List that is then written back to Firestore?
Create a FlutterFlow Custom Widget called PinchZoomViewer that wraps an image URL in Flutter's InteractiveViewer widget with minScale 0.5 and maxScale 4.0. Include a TransformationController and a double-tap gesture that animates back to the identity matrix (no zoom) if the user is zoomed in, or zooms to 2x if they are at the default scale. Use CachedNetworkImage for the image.
Frequently asked questions
Can I add swipe-left and swipe-right gestures without a Custom Widget in FlutterFlow?
FlutterFlow's PageView widget handles horizontal swipe natively for multi-page carousels. For swipe-to-dismiss or swipe-to-reveal on list items, you need a Custom Widget using Flutter's Dismissible or the flutter_slidable package, as FlutterFlow has no built-in swipe action for ListView items.
How do I detect a swipe gesture in a specific direction (left, right, up, down)?
Use GestureDetector with separate callbacks: onHorizontalDragEnd for left-right swipes (check velocity.pixelsPerSecond.dx sign) and onVerticalDragEnd for up-down swipes (check velocity.pixelsPerSecond.dy sign). Set a minimum velocity threshold (e.g., 300 pixels/second) to distinguish intentional swipes from slow drags.
Does FlutterFlow support force touch or pressure-sensitive gestures?
No. FlutterFlow and Flutter's standard gesture system do not expose force touch (3D Touch on older iPhones) or pressure-sensitive data. These require platform-specific code using Method Channels, which goes beyond FlutterFlow's custom action capabilities.
Why does my tap gesture not work inside a scrollable widget?
Scroll widgets use a delayed tap recognizer to distinguish a tap from the start of a scroll. There is typically a 100-200ms delay before a tap is confirmed. If you need instant feedback, add a visual effect on onTapDown (which fires immediately) and perform the action on onTap. Alternatively, use HitTestBehavior.opaque on your GestureDetector to claim the gesture immediately.
How do I implement a pull-to-refresh gesture in FlutterFlow?
FlutterFlow has a built-in RefreshIndicator widget under Layout widgets. Wrap your ListView or GridView in a RefreshIndicator and set its On Refresh action to reload your Firestore query and update the Page State variable. This provides the standard pull-down spinner that users expect on both iOS and Android.
Can I use gestures to draw on the screen, like a signature or sketch pad?
Yes, but it requires a Custom Widget. Use GestureDetector's onPanStart, onPanUpdate, and onPanEnd to capture a series of points as the user drags. Store the points in a List and use a CustomPainter to draw lines between consecutive points on a Canvas. This implements a basic signature or sketch pad. The flutter_drawing_board package provides a ready-made implementation.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation