# How to Implement Swipe Gestures in a FlutterFlow App

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 25-35 min
- Compatibility: FlutterFlow Free+ (Dismissible and PageView), FlutterFlow Pro+ (Custom Widget for card swipe)
- Last updated: March 2026

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

## Before you start

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

```
// Custom Widget: SwipeCard
// Parameters: onSwipeLeft (Action), onSwipeRight (Action), threshold (double)

class SwipeCard extends StatefulWidget {
  final Widget child;
  final Future<void> Function() onSwipeLeft;
  final Future<void> Function() onSwipeRight;
  final double threshold;

  const SwipeCard({
    Key? key,
    required this.child,
    required this.onSwipeLeft,
    required this.onSwipeRight,
    this.threshold = 100.0,
  }) : super(key: key);

  @override
  State<SwipeCard> createState() => _SwipeCardState();
}

class _SwipeCardState extends State<SwipeCard>
    with SingleTickerProviderStateMixin {
  double _dragOffsetX = 0.0;
  bool _isAnimating = false;
  late AnimationController _animController;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _animController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
  }

  @override
  void dispose() {
    _animController.dispose();
    super.dispose();
  }

  void _onHorizontalDragUpdate(DragUpdateDetails details) {
    if (_isAnimating) return;
    setState(() {
      _dragOffsetX += details.primaryDelta ?? 0;
    });
  }

  Future<void> _onHorizontalDragEnd(DragEndDetails details) async {
    if (_isAnimating) return;
    if (_dragOffsetX.abs() >= widget.threshold) {
      _isAnimating = true;
      final flyDirection = _dragOffsetX > 0 ? 800.0 : -800.0;
      _animation = Tween<double>(
        begin: _dragOffsetX,
        end: flyDirection,
      ).animate(CurvedAnimation(
        parent: _animController,
        curve: Curves.easeOut,
      ));
      _animController.addListener(() {
        setState(() => _dragOffsetX = _animation.value);
      });
      await _animController.forward();

      if (_dragOffsetX > 0) {
        await widget.onSwipeRight();
      } else {
        await widget.onSwipeLeft();
      }

      // Reset
      _animController.reset();
      setState(() {
        _dragOffsetX = 0.0;
        _isAnimating = false;
      });
    } else {
      // Snap back
      _animation = Tween<double>(
        begin: _dragOffsetX,
        end: 0.0,
      ).animate(CurvedAnimation(
        parent: _animController,
        curve: Curves.elasticOut,
      ));
      _animController.addListener(() {
        setState(() => _dragOffsetX = _animation.value);
      });
      await _animController.forward();
      _animController.reset();
      setState(() {
        _dragOffsetX = 0.0;
        _isAnimating = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    final rotation = _dragOffsetX / 800.0 * 0.3; // subtle rotation
    return GestureDetector(
      onHorizontalDragUpdate: _onHorizontalDragUpdate,
      onHorizontalDragEnd: _onHorizontalDragEnd,
      child: Transform.translate(
        offset: Offset(_dragOffsetX, 0),
        child: Transform.rotate(
          angle: rotation,
          child: widget.child,
        ),
      ),
    );
  }
}
```

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

```
// Addition to SwipeCard build method — add inside Transform.rotate child:

Stack(
  children: [
    widget.child,
    // Like overlay (swipe right)
    if (_dragOffsetX > 0)
      Positioned(
        top: 20,
        left: 20,
        child: Opacity(
          opacity: (_dragOffsetX / widget.threshold).clamp(0.0, 1.0),
          child: Container(
            padding: const EdgeInsets.all(8),
            decoration: BoxDecoration(
              border: Border.all(color: Colors.green, width: 3),
              borderRadius: BorderRadius.circular(8),
            ),
            child: const Text('LIKE',
                style: TextStyle(
                    color: Colors.green,
                    fontSize: 24,
                    fontWeight: FontWeight.bold)),
          ),
        ),
      ),
    // Dislike overlay (swipe left)
    if (_dragOffsetX < 0)
      Positioned(
        top: 20,
        right: 20,
        child: Opacity(
          opacity: (_dragOffsetX.abs() / widget.threshold).clamp(0.0, 1.0),
          child: Container(
            padding: const EdgeInsets.all(8),
            decoration: BoxDecoration(
              border: Border.all(color: Colors.red, width: 3),
              borderRadius: BorderRadius.circular(8),
            ),
            child: const Text('NOPE',
                style: TextStyle(
                    color: Colors.red,
                    fontSize: 24,
                    fontWeight: FontWeight.bold)),
          ),
        ),
      ),
  ],
)
```

**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 code example

File: `swipe_card_complete.dart`

```dart
// ============================================================
// FlutterFlow Swipe Card — Complete Custom Widget
// ============================================================

class SwipeCard extends StatefulWidget {
  final Widget child;
  final Future<void> Function() onSwipeLeft;
  final Future<void> Function() onSwipeRight;
  final double threshold;

  const SwipeCard({
    Key? key,
    required this.child,
    required this.onSwipeLeft,
    required this.onSwipeRight,
    this.threshold = 100.0,
  }) : super(key: key);

  @override
  State<SwipeCard> createState() => _SwipeCardState();
}

class _SwipeCardState extends State<SwipeCard>
    with SingleTickerProviderStateMixin {
  double _dx = 0.0;
  bool _busy = false;
  late final AnimationController _ctrl;

  @override
  void initState() {
    super.initState();
    _ctrl = AnimationController(
        vsync: this, duration: const Duration(milliseconds: 280));
  }

  @override
  void dispose() {
    _ctrl.dispose();
    super.dispose();
  }

  Future<void> _animateTo(double end, VoidCallback? onDone) async {
    final anim = Tween(begin: _dx, end: end).animate(
        CurvedAnimation(parent: _ctrl, curve: Curves.easeOut));
    anim.addListener(() => setState(() => _dx = anim.value));
    await _ctrl.forward();
    _ctrl.reset();
    onDone?.call();
  }

  @override
  Widget build(BuildContext context) {
    final rot = (_dx / 1200).clamp(-0.35, 0.35);
    final likeOpacity = (_dx / widget.threshold).clamp(0.0, 1.0);
    final nopeOpacity = (-_dx / widget.threshold).clamp(0.0, 1.0);

    return GestureDetector(
      onHorizontalDragUpdate: _busy
          ? null
          : (d) => setState(() => _dx += d.primaryDelta ?? 0),
      onHorizontalDragEnd: _busy
          ? null
          : (d) async {
              if (_dx.abs() < widget.threshold) {
                _busy = true;
                await _animateTo(0.0, () => setState(() => _busy = false));
                return;
              }
              _busy = true;
              final isRight = _dx > 0;
              await _animateTo(isRight ? 1000 : -1000, null);
              if (isRight) await widget.onSwipeRight();
              else await widget.onSwipeLeft();
              setState(() { _dx = 0.0; _busy = false; });
            },
      child: Transform(
        transform: Matrix4.identity()
          ..translate(_dx)
          ..rotateZ(rot),
        alignment: Alignment.bottomCenter,
        child: Stack(
          children: [
            widget.child,
            if (likeOpacity > 0)
              Positioned(top: 20, left: 20,
                child: Opacity(opacity: likeOpacity,
                  child: _label('LIKE', Colors.green))),
            if (nopeOpacity > 0)
              Positioned(top: 20, right: 20,
                child: Opacity(opacity: nopeOpacity,
                  child: _label('NOPE', Colors.red))),
          ],
        ),
      ),
    );
  }

  Widget _label(String text, Color color) => Container(
    padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
    decoration: BoxDecoration(
      border: Border.all(color: color, width: 3),
      borderRadius: BorderRadius.circular(8),
    ),
    child: Text(text,
      style: TextStyle(
          color: color, fontSize: 22, fontWeight: FontWeight.bold)),
  );
}
```

## Common mistakes

- **Adding a swipe gesture on a widget nested inside a horizontal ListView or PageView — causing gesture conflict** — Flutter's gesture recognizer arena means only one gesture handler wins. If a SwipeCard is inside a horizontal PageView, both try to claim the same horizontal drag gesture. The parent PageView usually wins, so the child SwipeCard never receives the gesture. The swipe appears to do nothing. Fix: 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.
- **Forgetting to set a unique Key on Dismissible widgets in a Repeating Group** — Dismissible uses the Key to track which item was dismissed. Without a unique key per item, Flutter may dismiss the wrong widget in the list, causing visual glitches (wrong item disappears, items jump) and potentially deleting wrong data from Firestore. Fix: 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.
- **Not resetting the drag offset after a swipe completes, causing subsequent cards to start mid-swipe** — In a stateful Custom Widget, the _dx (drag offset) variable persists in state. If you reuse the same SwipeCard widget instance for the next card without resetting _dx, the card appears displaced from center when it is first shown. Fix: 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.
- **Setting the swipe threshold too low (under 50px), causing accidental dismissals** — A threshold under 50px means users accidentally trigger swipes while scrolling or tapping imprecisely. This is especially problematic on small phone screens where accidental swipes frequently occur during normal scrolling. Fix: 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.

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

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-implement-swipe-gestures-in-a-flutterflow-app
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-implement-swipe-gestures-in-a-flutterflow-app
