image

31 Mar 2026

9K

35K

Creating an Interactive Flashcard Widget with Swipe, Flip, and Animation in Flutter

Flashcards are a powerful tool for learning and memorization, and building them into a mobile application can significantly enhance the user experience. In Flutter, we can create dynamic and engaging flashcard widgets that not only display information but also incorporate interactive gestures like swiping to navigate and tapping to flip, all enhanced with smooth animations.

This article will guide you through building a Flutter flashcard widget with the following features:

  • A customizable flashcard front and back.
  • A delightful 3D flip animation when tapped.
  • A smooth swipe gesture to dismiss the current card and reveal the next one in a stack.

Core Concepts

Before diving into the code, let's understand the core Flutter concepts we'll be utilizing:

  • State Management: We'll manage the flip state of individual cards and the current card index in a deck.
  • Animations: We'll use AnimationController, Tween, and `Transform.rotate` for the flip animation, and `Transform.translate` for swipe gestures.
  • Gestures: GestureDetector will be crucial for handling tap events (for flipping) and pan events (for swiping).
  • Widget Composition: We'll build a reusable Flashcard widget and a parent FlashcardDeck widget to manage a collection of cards.

Step 1: The Basic Flashcard Structure

First, let's create a basic `Flashcard` widget that can display content on its front and back. This widget will be stateful because it needs to manage its own flip state.


import 'package:flutter/material.dart';
import 'dart:math' as math;

class Flashcard extends StatefulWidget {
  final Widget front;
  final Widget back;

  const Flashcard({
    Key? key,
    required this.front,
    required this.back,
  }) : super(key: key);

  @override
  _FlashcardState createState() => _FlashcardState();
}

class _FlashcardState extends State with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation _animation;
  bool _isFront = true;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 600),
    );
    _animation = Tween(begin: 0, end: 1).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
  }

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

  void _flipCard() {
    if (_isFront) {
      _controller.forward();
    } else {
      _controller.reverse();
    }
    _isFront = !_isFront;
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _flipCard,
      child: AnimatedBuilder(
        animation: _animation,
        builder: (context, child) {
          final double rotation = _animation.value * math.pi; // Rotate 180 degrees
          final double opacity = rotation <= (math.pi / 2) ? (1 - _animation.value) : _animation.value;

          return Transform(
            alignment: Alignment.center,
            transform: Matrix4.identity()
              ..setEntry(3, 2, 0.001) // Perspective
              ..rotateY(rotation),
            child: Stack(
              children: [
                if (rotation <= math.pi / 2) // Show front side
                  _buildSide(widget.front, rotation)
                else // Show back side
                  Transform(
                    alignment: Alignment.center,
                    transform: Matrix4.identity().rotateY(math.pi), // Rotate back side 180 degrees
                    child: _buildSide(widget.back, rotation),
                  ),
              ],
            ),
          );
        },
      ),
    );
  }

  Widget _buildSide(Widget content, double rotation) {
    return Card(
      elevation: 8,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
      child: Container(
        width: double.infinity,
        height: double.infinity,
        padding: const EdgeInsets.all(20),
        alignment: Alignment.center,
        child: content,
      ),
    );
  }
}

In this `Flashcard` widget:

  • We use `SingleTickerProviderStateMixin` and `AnimationController` for precise animation control.
  • `_animation` goes from 0 to 1, representing a full 180-degree rotation (math.pi).
  • `Transform` widget with `Matrix4.identity()..setEntry(3, 2, 0.001)` creates a 3D perspective effect.
  • We check `rotation <= math.pi / 2` to determine whether to show the front or back side during the flip. The back side is also rotated 180 degrees initially so it appears correctly after the parent transform.
  • `GestureDetector` is used to trigger the `_flipCard` method on tap.

Step 2: Building the Flashcard Deck with Swipe Functionality

Now, let's create a `FlashcardDeck` widget that will manage a list of flashcards, display them in a stack, and enable swiping to navigate between them.


import 'package:flutter/material.dart';
import 'dart:math' as math;

// Assuming the Flashcard widget from Step 1 is in flashcard.dart
// import 'flashcard.dart'; 

class FlashcardDeck extends StatefulWidget {
  final List cards;

  const FlashcardDeck({
    Key? key,
    required this.cards,
  }) : super(key: key);

  @override
  _FlashcardDeckState createState() => _FlashcardDeckState();
}

class _FlashcardDeckState extends State with TickerProviderStateMixin {
  late AnimationController _swipeAnimationController;
  late Animation _swipeAnimation;
  late Animation _rotationAnimation;
  late Animation _scaleAnimation;

  int _currentIndex = 0;
  Offset _dragOffset = Offset.zero;

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

    _swipeAnimationController.addListener(() {
      if (_swipeAnimationController.isCompleted) {
        setState(() {
          _dragOffset = Offset.zero;
          _currentIndex = (_currentIndex + 1) % widget.cards.length; // Loop through cards
        });
        _swipeAnimationController.reset();
      }
    });

    _swipeAnimation = Tween(
      begin: Offset.zero,
      end: const Offset(500, 0), // Swiped off-screen
    ).animate(
      CurvedAnimation(
        parent: _swipeAnimationController,
        curve: Curves.easeOut,
      ),
    );

    // Animations for the next card appearing
    _scaleAnimation = Tween(begin: 0.9, end: 1.0).animate(
      CurvedAnimation(
        parent: _swipeAnimationController,
        curve: Curves.easeOut,
      ),
    );

    _rotationAnimation = Tween(begin: 0, end: 0).animate( // Will be updated on drag
      CurvedAnimation(
        parent: _swipeAnimationController,
        curve: Curves.easeOut,
      ),
    );
  }

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

  void _onPanUpdate(DragUpdateDetails details) {
    setState(() {
      _dragOffset += details.delta;
    });
  }

  void _onPanEnd(DragEndDetails details) {
    final double velocity = details.velocity.pixelsPerSecond.dx;
    final double displacement = _dragOffset.dx;

    if (displacement.abs() > 100 || velocity.abs() > 500) {
      // Swipe card off screen
      double endX = displacement > 0 ? 500 : -500;
      _swipeAnimation = Tween(
        begin: _dragOffset,
        end: Offset(endX, 0),
      ).animate(
        CurvedAnimation(
          parent: _swipeAnimationController,
          curve: Curves.easeOut,
        ),
      );
      _swipeAnimationController.forward();
    } else {
      // Return card to original position
      setState(() {
        _dragOffset = Offset.zero;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        return Stack(
          alignment: Alignment.center,
          children: List.generate(widget.cards.length, (index) {
            final card = widget.cards[index];
            if (index == _currentIndex) {
              return AnimatedBuilder(
                animation: _swipeAnimationController,
                builder: (context, child) {
                  // Calculate live offset during drag
                  final currentOffset = _swipeAnimationController.isAnimating
                      ? _swipeAnimation.value
                      : _dragOffset;

                  // Calculate rotation based on drag offset
                  final double rotationAngle = currentOffset.dx / constraints.maxWidth * 0.2; // Max 0.2 rad rotation

                  return Positioned(
                    left: currentOffset.dx,
                    top: currentOffset.dy.clamp(-50, 50), // Slight vertical movement allowed
                    child: Transform.rotate(
                      angle: rotationAngle,
                      child: GestureDetector(
                        onPanUpdate: _onPanUpdate,
                        onPanEnd: _onPanEnd,
                        child: SizedBox(
                          width: constraints.maxWidth * 0.8,
                          height: constraints.maxHeight * 0.7,
                          child: card,
                        ),
                      ),
                    ),
                  );
                },
              );
            } else if (index == (_currentIndex + 1) % widget.cards.length) {
              // The next card in the stack, scale it slightly
              return AnimatedBuilder(
                animation: _swipeAnimationController,
                builder: (context, child) {
                  return Positioned(
                    left: 0,
                    top: 0,
                    child: Transform.scale(
                      scale: _scaleAnimation.value,
                      child: Opacity(
                        opacity: _scaleAnimation.value - 0.7, // Appear smoothly
                        child: SizedBox(
                          width: constraints.maxWidth * 0.8,
                          height: constraints.maxHeight * 0.7,
                          child: card,
                        ),
                      ),
                    ),
                  );
                },
              );
            } else {
              return const SizedBox.shrink(); // Hide other cards
            }
          }).reversed.toList(), // Ensure current card is on top
        );
      },
    );
  }
}

Explanation of `FlashcardDeck`:

  • `_swipeAnimationController` manages the animation for cards flying off-screen or returning to center.
  • `_currentIndex` keeps track of the card currently at the top of the stack.
  • `_dragOffset` stores the live drag position of the top card.
  • `_onPanUpdate` updates `_dragOffset` as the user drags.
  • `_onPanEnd` checks if the drag distance or velocity is sufficient to dismiss the card. If so, it animates the card off-screen and increments `_currentIndex`. Otherwise, the card animates back to its center position.
  • The `Stack` widget is used to layer cards. `List.generate(...).reversed.toList()` ensures that cards with higher indices are rendered below those with lower indices, creating a natural stack appearance, but the current card is explicitly drawn on top.
  • `Positioned` and `Transform.rotate` are used on the current card to move and rotate it dynamically based on `_dragOffset`.
  • `_scaleAnimation` is applied to the *next* card in the stack to give it a subtle "pop" effect as it comes into view.

Step 3: Integrating into Your Application

Finally, let's put it all together in your main application widget.


import 'package:flutter/material.dart';
// import 'flashcard.dart'; // Make sure Flashcard is accessible
// import 'flashcard_deck.dart'; // Make sure FlashcardDeck is accessible

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flashcard App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const FlashcardScreen(),
    );
  }
}

class FlashcardScreen extends StatelessWidget {
  const FlashcardScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final List cards = [
      Flashcard(
        front: const Text(
          'What is Flutter?',
          style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          textAlign: TextAlign.center,
        ),
        back: const Text(
          'A UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase.',
          style: TextStyle(fontSize: 18),
          textAlign: TextAlign.center,
        ),
      ),
      Flashcard(
        front: const Text(
          'Who developed Flutter?',
          style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          textAlign: TextAlign.center,
        ),
        back: const Text(
          'Google',
          style: TextStyle(fontSize: 18),
          textAlign: TextAlign.center,
        ),
      ),
      Flashcard(
        front: const Text(
          'What is a Widget?',
          style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          textAlign: TextAlign.center,
        ),
        back: const Text(
          'The basic building blocks of a Flutter UI. Everything is a widget!',
          style: TextStyle(fontSize: 18),
          textAlign: TextAlign.center,
        ),
      ),
    ];

    return Scaffold(
      appBar: AppBar(
        title: const Text('Interactive Flashcards'),
        backgroundColor: Colors.deepPurple,
      ),
      body: Center(
        child: SizedBox(
          width: MediaQuery.of(context).size.width * 0.9,
          height: MediaQuery.of(context).size.height * 0.7,
          child: FlashcardDeck(cards: cards),
        ),
      ),
    );
  }
}

Run this Flutter application, and you'll have an interactive flashcard deck where you can tap to flip cards and swipe to dismiss them, revealing the next one in the stack!

Further Enhancements

  • Swipe Direction Logic: Differentiate between left and right swipes (e.g., "know" vs. "don't know") by checking `_dragOffset.dx` sign.
  • Undo Functionality: Add a button to bring back the last swiped card.
  • Dynamic Content Loading: Load flashcard data from a database or API.
  • Accessibility: Ensure the widget is accessible for all users.
  • More Complex Animations: Add more intricate animations, such as a "spring" effect when cards return to center.

By following these steps, you've created a robust and visually appealing flashcard widget that significantly enhances user engagement through interactive gestures and smooth animations in Flutter.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is