FlutterFlow supports swipe gestures through multiple approaches: Dismissible widget (built into FlutterFlow) for swipe-to-delete in lists, PageView for horizontal swipe navigation between screens, and a Custom Widget using GestureDetector and AnimatedPositioned for Tinder-style card swiping with directional animations. The most common mistake is adding swipe on widgets nested inside a horizontal scrolling container — causing gesture conflicts.
Three Types of Swipe Gestures in FlutterFlow
Swipe interactions range from subtle (swipe a list item to reveal delete) to dramatic (swipe a card left/right to reject/accept). Each requires a different Flutter widget. FlutterFlow exposes Dismissible natively in the widget palette. PageView and custom card swiping require more setup. Understanding which approach fits your use case saves significant development time.
Prerequisites
- A FlutterFlow project with at least one page and a list or card UI built
- Basic understanding of FlutterFlow widget tree and Action Flows
- Optional: familiarity with Custom Widgets for the Tinder-card section
Step-by-step guide
Add Swipe-to-Delete with the Dismissible Widget
Add Swipe-to-Delete with the Dismissible Widget
In FlutterFlow's widget palette (left sidebar, '+' button), search for 'Dismissible'. Drag it into your Repeating Group item builder. Dismissible wraps your list item widget and provides swipe-left/right gestures. In the Dismissible's Properties panel, set: 'Key' to a unique identifier for each item (bind to the Firestore document reference or item ID — this is required for Dismissible to function correctly), 'Direction' to horizontal (left, right, or both), 'Background' (what shows behind the item when swiping — typically a red delete icon), and 'On Dismiss' action (wire to Delete Document → the current item's Firestore reference). You can set different backgrounds for left and right swipe directions — for example, red/trash on left swipe (delete) and green/archive on right swipe (archive).
Expected result: Swiping a list item reveals a colored background with an icon. Completing the swipe removes the item from the list and triggers the On Dismiss action (e.g., deletes the Firestore document).
Build Horizontal Swipe Navigation with PageView
Build Horizontal Swipe Navigation with PageView
For swiping between full pages or large content cards (like onboarding screens or image galleries), use PageView. In FlutterFlow's widget palette, search for 'PageView'. Drag it onto your page canvas. Set its Width to fill the page (or container). Inside the PageView, add your page items as children — each becomes one swipeable 'page'. In the PageView Properties, set: 'Axis' (horizontal or vertical), 'Enable Infinite Scroll' (loops back to first page after last), and 'Initial Page Index'. You can add a 'Page Controller' to programmatically navigate to specific pages from an Action Flow — for example, a 'Next' button that calls PageView.animateToPage(). Add dot indicator widgets below the PageView that update based on the current page index via a Page State variable.
Expected result: Users can swipe left and right between pages. A dot indicator updates to show the current position. A 'Next' button navigates programmatically.
Create a Tinder-Style Card Swipe Custom Widget
Create a Tinder-Style Card Swipe Custom Widget
Tinder-style card swiping (swipe left to reject, right to accept) requires a Custom Widget using GestureDetector and AnimatedPositioned. The widget detects horizontal drag distance and direction, animates the card accordingly (fly off left or right), calls the appropriate callback action, and resets for the next card. This pattern powers dating apps, job matching apps, content discovery, and recommendation interfaces. Create a Custom Widget named 'SwipeCard'. Parameters: the child widget content (passed as a Widget), onSwipeLeft (Action), onSwipeRight (Action), and optional threshold (double, default 100.0 — the drag distance required to trigger a swipe).
1// Custom Widget: SwipeCard2// Parameters: onSwipeLeft (Action), onSwipeRight (Action), threshold (double)34class SwipeCard extends StatefulWidget {5 final Widget child;6 final Future<void> Function() onSwipeLeft;7 final Future<void> Function() onSwipeRight;8 final double threshold;910 const SwipeCard({11 Key? key,12 required this.child,13 required this.onSwipeLeft,14 required this.onSwipeRight,15 this.threshold = 100.0,16 }) : super(key: key);1718 @override19 State<SwipeCard> createState() => _SwipeCardState();20}2122class _SwipeCardState extends State<SwipeCard>23 with SingleTickerProviderStateMixin {24 double _dragOffsetX = 0.0;25 bool _isAnimating = false;26 late AnimationController _animController;27 late Animation<double> _animation;2829 @override30 void initState() {31 super.initState();32 _animController = AnimationController(33 vsync: this,34 duration: const Duration(milliseconds: 300),35 );36 }3738 @override39 void dispose() {40 _animController.dispose();41 super.dispose();42 }4344 void _onHorizontalDragUpdate(DragUpdateDetails details) {45 if (_isAnimating) return;46 setState(() {47 _dragOffsetX += details.primaryDelta ?? 0;48 });49 }5051 Future<void> _onHorizontalDragEnd(DragEndDetails details) async {52 if (_isAnimating) return;53 if (_dragOffsetX.abs() >= widget.threshold) {54 _isAnimating = true;55 final flyDirection = _dragOffsetX > 0 ? 800.0 : -800.0;56 _animation = Tween<double>(57 begin: _dragOffsetX,58 end: flyDirection,59 ).animate(CurvedAnimation(60 parent: _animController,61 curve: Curves.easeOut,62 ));63 _animController.addListener(() {64 setState(() => _dragOffsetX = _animation.value);65 });66 await _animController.forward();6768 if (_dragOffsetX > 0) {69 await widget.onSwipeRight();70 } else {71 await widget.onSwipeLeft();72 }7374 // Reset75 _animController.reset();76 setState(() {77 _dragOffsetX = 0.0;78 _isAnimating = false;79 });80 } else {81 // Snap back82 _animation = Tween<double>(83 begin: _dragOffsetX,84 end: 0.0,85 ).animate(CurvedAnimation(86 parent: _animController,87 curve: Curves.elasticOut,88 ));89 _animController.addListener(() {90 setState(() => _dragOffsetX = _animation.value);91 });92 await _animController.forward();93 _animController.reset();94 setState(() {95 _dragOffsetX = 0.0;96 _isAnimating = false;97 });98 }99 }100101 @override102 Widget build(BuildContext context) {103 final rotation = _dragOffsetX / 800.0 * 0.3; // subtle rotation104 return GestureDetector(105 onHorizontalDragUpdate: _onHorizontalDragUpdate,106 onHorizontalDragEnd: _onHorizontalDragEnd,107 child: Transform.translate(108 offset: Offset(_dragOffsetX, 0),109 child: Transform.rotate(110 angle: rotation,111 child: widget.child,112 ),113 ),114 );115 }116}Expected result: Dragging a card horizontally follows the gesture. Releasing past the threshold flies the card off screen and triggers the appropriate callback. Releasing before the threshold snaps the card back to center with a spring animation.
Add Like/Dislike Overlays to the Swipe Card
Add Like/Dislike Overlays to the Swipe Card
Enhance the SwipeCard widget with directional feedback overlays. Inside the Transform.translate widget in the build method, use a Stack to layer the child content and two overlay widgets on top. The left overlay shows a red circle with an 'X' icon; the right overlay shows a green circle with a heart icon. Set each overlay's opacity based on the drag offset magnitude and direction. When _dragOffsetX > 0 (swiping right), the heart icon fades from 0 to 1 opacity as _dragOffsetX increases from 0 to threshold. When _dragOffsetX < 0, the X icon fades in. This gives users immediate visual feedback about which action the swipe will trigger before they release their finger.
1// Addition to SwipeCard build method — add inside Transform.rotate child:23Stack(4 children: [5 widget.child,6 // Like overlay (swipe right)7 if (_dragOffsetX > 0)8 Positioned(9 top: 20,10 left: 20,11 child: Opacity(12 opacity: (_dragOffsetX / widget.threshold).clamp(0.0, 1.0),13 child: Container(14 padding: const EdgeInsets.all(8),15 decoration: BoxDecoration(16 border: Border.all(color: Colors.green, width: 3),17 borderRadius: BorderRadius.circular(8),18 ),19 child: const Text('LIKE',20 style: TextStyle(21 color: Colors.green,22 fontSize: 24,23 fontWeight: FontWeight.bold)),24 ),25 ),26 ),27 // Dislike overlay (swipe left)28 if (_dragOffsetX < 0)29 Positioned(30 top: 20,31 right: 20,32 child: Opacity(33 opacity: (_dragOffsetX.abs() / widget.threshold).clamp(0.0, 1.0),34 child: Container(35 padding: const EdgeInsets.all(8),36 decoration: BoxDecoration(37 border: Border.all(color: Colors.red, width: 3),38 borderRadius: BorderRadius.circular(8),39 ),40 child: const Text('NOPE',41 style: TextStyle(42 color: Colors.red,43 fontSize: 24,44 fontWeight: FontWeight.bold)),45 ),46 ),47 ),48 ],49)Expected result: As the user drags right, a green 'LIKE' label fades in. Dragging left shows a red 'NOPE' label. Both fade back to invisible if the user reverses direction.
Stack Multiple Cards for a Card Deck Effect
Stack Multiple Cards for a Card Deck Effect
A single swipeable card is functional, but the classic card deck visual (next card visible behind the current one, scaling up as the top card is swiped away) requires a Stack widget with multiple SwipeCard instances. In FlutterFlow, create a Custom Widget named 'CardDeck'. It takes a List of item data as a parameter. Internally, it renders a Stack with the top 3 items from the list. The bottom cards are scaled down slightly (0.95x, 0.90x) and offset vertically. When the top card is swiped, remove the first item from the list using setState, making the second card become the new top card and animate to full scale. This creates the satisfying 'card deck' animation seen in popular matching apps. Wire the onSwipeLeft and onSwipeRight callbacks to FlutterFlow App State updates and Firestore writes.
Expected result: The card deck shows the current card with two partially-visible cards behind it. Swiping the top card away reveals the next card scaling smoothly to full size.
Complete working example
1// ============================================================2// FlutterFlow Swipe Card — Complete Custom Widget3// ============================================================45class SwipeCard extends StatefulWidget {6 final Widget child;7 final Future<void> Function() onSwipeLeft;8 final Future<void> Function() onSwipeRight;9 final double threshold;1011 const SwipeCard({12 Key? key,13 required this.child,14 required this.onSwipeLeft,15 required this.onSwipeRight,16 this.threshold = 100.0,17 }) : super(key: key);1819 @override20 State<SwipeCard> createState() => _SwipeCardState();21}2223class _SwipeCardState extends State<SwipeCard>24 with SingleTickerProviderStateMixin {25 double _dx = 0.0;26 bool _busy = false;27 late final AnimationController _ctrl;2829 @override30 void initState() {31 super.initState();32 _ctrl = AnimationController(33 vsync: this, duration: const Duration(milliseconds: 280));34 }3536 @override37 void dispose() {38 _ctrl.dispose();39 super.dispose();40 }4142 Future<void> _animateTo(double end, VoidCallback? onDone) async {43 final anim = Tween(begin: _dx, end: end).animate(44 CurvedAnimation(parent: _ctrl, curve: Curves.easeOut));45 anim.addListener(() => setState(() => _dx = anim.value));46 await _ctrl.forward();47 _ctrl.reset();48 onDone?.call();49 }5051 @override52 Widget build(BuildContext context) {53 final rot = (_dx / 1200).clamp(-0.35, 0.35);54 final likeOpacity = (_dx / widget.threshold).clamp(0.0, 1.0);55 final nopeOpacity = (-_dx / widget.threshold).clamp(0.0, 1.0);5657 return GestureDetector(58 onHorizontalDragUpdate: _busy59 ? null60 : (d) => setState(() => _dx += d.primaryDelta ?? 0),61 onHorizontalDragEnd: _busy62 ? null63 : (d) async {64 if (_dx.abs() < widget.threshold) {65 _busy = true;66 await _animateTo(0.0, () => setState(() => _busy = false));67 return;68 }69 _busy = true;70 final isRight = _dx > 0;71 await _animateTo(isRight ? 1000 : -1000, null);72 if (isRight) await widget.onSwipeRight();73 else await widget.onSwipeLeft();74 setState(() { _dx = 0.0; _busy = false; });75 },76 child: Transform(77 transform: Matrix4.identity()78 ..translate(_dx)79 ..rotateZ(rot),80 alignment: Alignment.bottomCenter,81 child: Stack(82 children: [83 widget.child,84 if (likeOpacity > 0)85 Positioned(top: 20, left: 20,86 child: Opacity(opacity: likeOpacity,87 child: _label('LIKE', Colors.green))),88 if (nopeOpacity > 0)89 Positioned(top: 20, right: 20,90 child: Opacity(opacity: nopeOpacity,91 child: _label('NOPE', Colors.red))),92 ],93 ),94 ),95 );96 }9798 Widget _label(String text, Color color) => Container(99 padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),100 decoration: BoxDecoration(101 border: Border.all(color: color, width: 3),102 borderRadius: BorderRadius.circular(8),103 ),104 child: Text(text,105 style: TextStyle(106 color: color, fontSize: 22, fontWeight: FontWeight.bold)),107 );108}Common mistakes
Why it's a problem: Adding a swipe gesture on a widget nested inside a horizontal ListView or PageView — causing gesture conflict
How to avoid: Never nest horizontal swipe gestures inside other horizontal gesture widgets. If you need a card stack inside a scrollable page, make the scroll vertical (SingleChildScrollView or ListView with Axis.vertical) so the horizontal card gestures do not conflict. Alternatively, use a GestureDetector with HitTestBehavior.opaque on the card to force it to claim the gesture first.
Why it's a problem: Forgetting to set a unique Key on Dismissible widgets in a Repeating Group
How to avoid: Set the Dismissible Key property in FlutterFlow to the Firestore document ID of each list item. Bind it using the Expression Editor: current item document reference → documentId. This guarantees uniqueness across the list.
Why it's a problem: Not resetting the drag offset after a swipe completes, causing subsequent cards to start mid-swipe
How to avoid: Call setState(() { _dx = 0.0; }) after the swipe callback completes and before updating the visible item. If the card widget is rebuilt with new data (new item in the stack), ensure the Key changes too so Flutter creates a fresh state instance.
Why it's a problem: Setting the swipe threshold too low (under 50px), causing accidental dismissals
How to avoid: Set threshold to 80-120px for card swipes. For Dismissible, Flutter's built-in threshold is 40% of the widget width — this is generally appropriate. For custom GestureDetector implementations, 100px is a reasonable default that balances responsiveness and accidental activation.
Best practices
- Provide both swipe AND button alternatives for every swipe action — some users cannot perform precise swipes due to motor impairments or device size.
- Show visual affordance hints (a partially visible card edge, a swipe tutorial animation on first launch) so users discover the swipe interaction without being told verbally.
- Match swipe directions to user mental models: left = reject/delete/dismiss, right = accept/like/save. Reversing these is highly disorienting.
- Animate the next card scaling up as the current card is swiped away — the smooth transition reinforces the physical metaphor of a card deck.
- For swipe-to-delete, implement undo functionality — show a Snackbar with an 'Undo' button for 3-5 seconds after dismissal before committing the Firestore delete.
- Test swipe gestures on actual physical devices across screen sizes — what feels right on a large Android tablet may feel hypersensitive on a small iPhone SE.
- Disable swipe gestures while a previous swipe's action is processing (set _isAnimating to true) to prevent double-swipe race conditions.
- For card decks with large data sets, use lazy loading — only render 3-5 cards at a time and fetch the next batch from Firestore when the user is 2 cards from the end.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm building a Tinder-style card swipe feature in a FlutterFlow app. I need a Flutter Custom Widget that: detects horizontal drag gestures, animates the card to fly off screen in the drag direction if the drag exceeds a 100px threshold, shows 'LIKE' or 'NOPE' overlays that fade in based on drag direction and distance, snaps back to center if the drag is released under the threshold, and calls separate onSwipeLeft and onSwipeRight callbacks. Write the complete StatefulWidget Dart code.
In FlutterFlow, I want to add swipe-to-delete to a Repeating Group that displays Firestore documents. What widget do I use, what properties must I set (especially the Key), how do I configure the On Dismiss action to delete the Firestore document, and how do I add different background colors for left vs right swipe?
Frequently asked questions
Does FlutterFlow have a built-in swipe gesture widget?
FlutterFlow includes the Dismissible widget natively for swipe-to-delete/archive in lists. For other swipe interactions (PageView swiping, Tinder-style card swiping), you need to use FlutterFlow's PageView widget or create a Custom Widget using GestureDetector. There is no built-in card swipe or drag-to-accept widget in the current version.
How do I make a ListView swipeable to reveal action buttons (like iOS Mail)?
This pattern (swipe to reveal hidden action buttons) requires the flutter_slidable package, not Dismissible. Add it in Settings → Pubspec Dependencies, then create a Custom Widget that wraps your list item in a Slidable widget with ActionPane configurations for left and right swipe panels.
Can I add swipe-to-navigate between pages (like in a dating app) without a Custom Widget?
Yes — use FlutterFlow's built-in PageView widget for swiping between full pages. If you need a 'floating card' swipe that does not occupy the full screen, you need a Custom Widget. PageView only works for full-screen page transitions, not partial card overlays.
Why does my swipe gesture stop working after the first swipe?
The most common cause is state not resetting after the swipe animation completes. The _isAnimating flag remains true, blocking all future gesture inputs. Ensure you always reset _isAnimating (or _busy) to false in a finally block or after the animation callback — even if the swipe callback throws an error.
Can I use swipe gestures on widgets inside a FlutterFlow Column or Row?
Yes — there is no gesture conflict between a horizontal swipe widget and a vertical Column/Row layout. The conflict only arises when a horizontal swipe is nested inside another horizontal gesture consumer (horizontal ListView, PageView, or horizontal SingleChildScrollView). Vertical containers do not claim horizontal gestures.
How do I programmatically trigger a swipe animation in FlutterFlow?
You need a GlobalKey on the SwipeCard Custom Widget state, and expose a public method (like swipeLeft() and swipeRight()) on the state class. From a Custom Action, access the state via the GlobalKey and call the method. This is an advanced Flutter pattern — simpler alternatives include using a Page State boolean that the widget watches via didUpdateWidget() to trigger the animation.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation