image

08 Jan 2026

9K

35K

Flutter Flip Card Animation for Quiz App

Creating engaging user interfaces is crucial for modern mobile applications. In quiz applications, dynamic elements can significantly enhance the user experience, making learning more interactive and fun. A flip card animation, where a card smoothly rotates to reveal its other side, is a perfect candidate for displaying questions and answers in a visually appealing manner.

This article will guide you through implementing a customizable flip card animation in Flutter, specifically tailored for a quiz application context. We'll cover the core animation concepts and how to integrate this component into a functional quiz flow.

Core Animation Concepts in Flutter

Flutter's animation system is powerful and flexible, built around several key components:

  • AnimationController: Manages the animation's state, starting, stopping, and reversing it. It produces values ranging from 0.0 to 1.0 over a specified duration.
  • Tween: Defines the range of values an animation should produce (e.g., 0 to 180 degrees for rotation). It interpolates values between a begin and end point.
  • CurvedAnimation: Applies a non-linear curve to an AnimationController's output, allowing for effects like ease-in, ease-out, or bounce.
  • AnimatedBuilder: A performance optimization widget that rebuilds only the animating part of the widget tree when the animation's value changes, avoiding unnecessary full widget tree rebuilds.
  • Transform: A widget that applies a transformation (like rotation, scaling, or translation) to its child. For 3D effects, Matrix4 is used to define the transformation matrix, including perspective.

Implementing the Flip Card Widget

We'll create a reusable FlipCard widget that can display any two widgets as its front and back sides. The card's flip state will be controlled externally, making it easy to integrate into a quiz application.

1. Create the FlipCard Widget

This widget will be a StatefulWidget because it manages its own animation state. It will take front and back widgets, an isFlipped boolean to control its state, and optional parameters for animation duration and curve.


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

class FlipCard extends StatefulWidget {
  final Widget front;
  final Widget back;
  final bool isFlipped;
  final Duration animationDuration;
  final Curve animationCurve;

  const FlipCard({
    Key? key,
    required this.front,
    required this.back,
    this.isFlipped = false,
    this.animationDuration = const Duration(milliseconds: 600),
    this.animationCurve = Curves.easeInOut,
  }) : super(key: key);

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

class _FlipCardState extends State with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation _animation;
  bool _showFront = true; // Tracks which side is currently visible during the flip

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: widget.animationDuration,
    );
    _animation = Tween(begin: 0, end: 1).animate(
      CurvedAnimation(parent: _controller, curve: widget.animationCurve),
    );

    // Initialize flip state if the card starts flipped
    if (widget.isFlipped) {
      _controller.value = 1;
      _showFront = false;
    }

    // Listener to switch between front and back child during the animation
    _controller.addListener(() {
      if (_controller.value >= 0.5) {
        if (_showFront) {
          setState(() {
            _showFront = false;
          });
        }
      } else {
        if (!_showFront) {
          setState(() {
            _showFront = true;
          });
        }
      }
    });
  }

  @override
  void didUpdateWidget(covariant FlipCard oldWidget) {
    super.didUpdateWidget(oldWidget);
    // Respond to changes in the isFlipped property from the parent widget
    if (widget.isFlipped != oldWidget.isFlipped) {
      if (widget.isFlipped) {
        _controller.forward();
      } else {
        _controller.reverse();
      }
    }
    // Update animation duration/curve if they change
    if (widget.animationDuration != oldWidget.animationDuration) {
      _controller.duration = widget.animationDuration;
    }
    if (widget.animationCurve != oldWidget.animationCurve) {
      _animation = Tween(begin: 0, end: 1).animate(
        CurvedAnimation(parent: _controller, curve: widget.animationCurve),
      );
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        // Calculate the rotation angle based on the animation value
        final double rotation = _animation.value * math.pi; // Rotate 180 degrees (pi radians)

        return Transform(
          alignment: FractionalOffset.center,
          transform: Matrix4.identity()
            ..setEntry(3, 2, 0.001) // Apply perspective for a 3D effect
            ..rotateY(rotation), // Rotate around the Y-axis
          child: _showFront
              ? widget.front
              : Transform(
                  alignment: FractionalOffset.center,
                  transform: Matrix4.identity()..rotateY(math.pi), // Rotate the back side an additional 180 degrees to face front
                  child: widget.back,
                ),
        );
      },
    );
  }
}

Explanation of Key Parts:

  • _FlipCardState with SingleTickerProviderStateMixin: This mixin is essential for AnimationController to function, providing a "ticker" that drives the animation frames.
  • initState(): Initializes the AnimationController and Tween. The _animation interpolates from 0 to 1. An important listener is added to `_controller` to switch the `_showFront` boolean when the animation passes the halfway point (0.5). This ensures the correct child (`front` or `back`) is rendered to prevent rendering glitches during the flip.
  • didUpdateWidget(): This method is crucial for reacting to changes in the `isFlipped` property passed from the parent. If `isFlipped` changes, it either calls `_controller.forward()` to flip to the back or `_controller.reverse()` to flip back to the front.
  • build() with AnimatedBuilder and Transform:
    • AnimatedBuilder rebuilds only its `builder` function when the `_animation` value changes, making it efficient.
    • Transform applies the 3D rotation. `Matrix4.identity()` creates a 4x4 identity matrix.
    • `setEntry(3, 2, 0.001)` adds a perspective effect, making the card appear to recede or protrude as it rotates.
    • `rotateY(rotation)` applies the rotation around the Y-axis. The `_animation.value` (0.0 to 1.0) is multiplied by `math.pi` (180 degrees) to get the desired rotation angle.
    • The conditional rendering (`_showFront ? widget.front : ...`) ensures that only the currently visible side is rendered. The `back` widget is also wrapped in a `Transform` with `rotateY(math.pi)` to ensure it is initially rendered "face-down" (rotated 180 degrees) so it appears correctly when flipped into view.

Integrating into a Quiz Application

Now, let's see how this FlipCard widget can be used in a simple quiz application. The quiz app will manage the current question and the flip state of the card.

2. Quiz Screen Implementation

The parent widget (e.g., a QuizScreen) will be a StatefulWidget that holds the current question data and controls the isFlipped state of the FlipCard.


import 'package:flutter/material.dart';
// Make sure to import your FlipCard widget
// import 'path/to/flip_card.dart'; 

class QuizScreen extends StatefulWidget {
  const QuizScreen({Key? key}) : super(key: key);

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

class _QuizScreenState extends State {
  bool _isCardFlipped = false;
  int _currentQuestionIndex = 0;

  final List<Map<String, String>> _questions = [
    {'question': 'What is the capital of France?', 'answer': 'Paris'},
    {'question': 'Who painted the Mona Lisa?', 'answer': 'Leonardo da Vinci'},
    {'question': 'What is the chemical symbol for water?', 'answer': 'H2O'},
  ];

  void _flipCard() {
    setState(() {
      _isCardFlipped = !_isCardFlipped;
    });
  }

  void _nextQuestion() {
    setState(() {
      _isCardFlipped = false; // Reset flip state for new question
      _currentQuestionIndex = (_currentQuestionIndex + 1) % _questions.length;
    });
  }

  @override
  Widget build(BuildContext context) {
    final currentQuestion = _questions[_currentQuestionIndex];

    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Quiz App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            SizedBox(
              width: 300,
              height: 200,
              child: GestureDetector(
                onTap: _flipCard, // Tap to flip the card
                child: FlipCard(
                  isFlipped: _isCardFlipped,
                  front: Card(
                    color: Colors.blueAccent,
                    elevation: 5,
                    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
                    child: Center(
                      child: Padding(
                        padding: const EdgeInsets.all(16.0),
                        child: Text(
                          currentQuestion['question']!,
                          textAlign: TextAlign.center,
                          style: const TextStyle(fontSize: 20, color: Colors.white, fontWeight: FontWeight.bold),
                        ),
                      ),
                    ),
                  ),
                  back: Card(
                    color: Colors.green,
                    elevation: 5,
                    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
                    child: Center(
                      child: Padding(
                        padding: const EdgeInsets.all(16.0),
                        child: Text(
                          currentQuestion['answer']!,
                          textAlign: TextAlign.center,
                          style: const TextStyle(fontSize: 20, color: Colors.white, fontWeight: FontWeight.bold),
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            ),
            const SizedBox(height: 30),
            ElevatedButton(
              onPressed: _nextQuestion,
              style: ElevatedButton.styleFrom(
                padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 15),
                textStyle: const TextStyle(fontSize: 18),
              ),
              child: const Text('Next Question'),
            ),
          ],
        ),
      ),
    );
  }
}

In this quiz app example:

  • The `_isCardFlipped` boolean state controls whether the `FlipCard` shows its front (question) or back (answer).
  • Tapping the `GestureDetector` wrapped around the `FlipCard` toggles this boolean, triggering the flip animation.
  • The "Next Question" button resets `_isCardFlipped` to `false` and advances to the next question in the `_questions` list.
  • The `front` and `back` properties of `FlipCard` are simple `Card` widgets containing the question and answer text, respectively. These can be customized with any Flutter widgets.

Enhancements and Considerations

  • Customization: The `FlipCard` widget already supports custom `animationDuration` and `animationCurve`. You can further expose parameters for `alignment` or even different rotation axes (`rotateX`, `rotateZ`).
  • Accessibility: Ensure that the content on both sides of the card is accessible. Consider adding `Semantics` widgets if the visual flip is the primary means of conveying information.
  • Performance: For complex cards, ensure the content inside `front` and `back` widgets is optimized. `AnimatedBuilder` already helps by limiting rebuilds.
  • State Management: For larger quiz apps, consider integrating a state management solution (like Provider, Riverpod, BLoC, GetX) to manage quiz state (`_isCardFlipped`, `_currentQuestionIndex`, etc.) more robustly.
  • Multiple Cards: If you need multiple flip cards on the same screen, each would need its own `FlipCard` instance, and its `isFlipped` state would be managed independently.

Conclusion

Implementing a flip card animation in Flutter for a quiz app is an excellent way to add interactivity and a polished feel. By understanding Flutter's core animation widgets like `AnimationController`, `Tween`, `AnimatedBuilder`, and `Transform`, you can create a highly customizable and performant UI component. The modular `FlipCard` widget developed here provides a solid foundation that can be easily integrated and extended to fit various quiz designs and other interactive card-based interfaces.

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