Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Implement Swipe Gestures in a FlutterFlow App

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.

What you'll learn

  • How to use the Dismissible widget for swipe-to-delete and swipe-to-archive in lists
  • How to build a PageView for horizontal swipe navigation between full screens or cards
  • How to create a Tinder-style swipe card Custom Widget with directional animation
  • How to avoid gesture conflicts when nesting swipeable widgets inside scrolling containers
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner12 min read25-35 minFlutterFlow Free+ (Dismissible and PageView), FlutterFlow Pro+ (Custom Widget for card swipe)March 2026RapidDev Engineering Team
TL;DR

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

1

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).

2

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.

3

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).

swipe_card.dart
1// Custom Widget: SwipeCard
2// Parameters: onSwipeLeft (Action), onSwipeRight (Action), threshold (double)
3
4class SwipeCard extends StatefulWidget {
5 final Widget child;
6 final Future<void> Function() onSwipeLeft;
7 final Future<void> Function() onSwipeRight;
8 final double threshold;
9
10 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);
17
18 @override
19 State<SwipeCard> createState() => _SwipeCardState();
20}
21
22class _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;
28
29 @override
30 void initState() {
31 super.initState();
32 _animController = AnimationController(
33 vsync: this,
34 duration: const Duration(milliseconds: 300),
35 );
36 }
37
38 @override
39 void dispose() {
40 _animController.dispose();
41 super.dispose();
42 }
43
44 void _onHorizontalDragUpdate(DragUpdateDetails details) {
45 if (_isAnimating) return;
46 setState(() {
47 _dragOffsetX += details.primaryDelta ?? 0;
48 });
49 }
50
51 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();
67
68 if (_dragOffsetX > 0) {
69 await widget.onSwipeRight();
70 } else {
71 await widget.onSwipeLeft();
72 }
73
74 // Reset
75 _animController.reset();
76 setState(() {
77 _dragOffsetX = 0.0;
78 _isAnimating = false;
79 });
80 } else {
81 // Snap back
82 _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 }
100
101 @override
102 Widget build(BuildContext context) {
103 final rotation = _dragOffsetX / 800.0 * 0.3; // subtle rotation
104 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.

4

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.

swipe_card_overlays.dart
1// Addition to SwipeCard build method — add inside Transform.rotate child:
2
3Stack(
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.

5

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

swipe_card_complete.dart
1// ============================================================
2// FlutterFlow Swipe Card — Complete Custom Widget
3// ============================================================
4
5class SwipeCard extends StatefulWidget {
6 final Widget child;
7 final Future<void> Function() onSwipeLeft;
8 final Future<void> Function() onSwipeRight;
9 final double threshold;
10
11 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);
18
19 @override
20 State<SwipeCard> createState() => _SwipeCardState();
21}
22
23class _SwipeCardState extends State<SwipeCard>
24 with SingleTickerProviderStateMixin {
25 double _dx = 0.0;
26 bool _busy = false;
27 late final AnimationController _ctrl;
28
29 @override
30 void initState() {
31 super.initState();
32 _ctrl = AnimationController(
33 vsync: this, duration: const Duration(milliseconds: 280));
34 }
35
36 @override
37 void dispose() {
38 _ctrl.dispose();
39 super.dispose();
40 }
41
42 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 }
50
51 @override
52 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);
56
57 return GestureDetector(
58 onHorizontalDragUpdate: _busy
59 ? null
60 : (d) => setState(() => _dx += d.primaryDelta ?? 0),
61 onHorizontalDragEnd: _busy
62 ? null
63 : (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 }
97
98 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.

ChatGPT Prompt

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.

FlutterFlow Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.