All widgets
Widget

Flip Card Widget

A beautiful 3D flip card widget with smooth animations for FlutterFlow. Tap to reveal the back side with a satisfying flip animation — fully customizable colors, text, direction, and speed.

FlutterDartFlutterFlow
View on GitHub

Interactive demo

Try it yourself ↓

Tap to flip ✦tap to flip
Revealed! 🎉tap to flip back

What's included

Features

  • 🔄3D flip animation — realistic perspective transform with easeInOutBack curve
  • ↔️Horizontal & vertical flip — choose your direction
  • 🎨Fully customizable — colors, text, icons, border radius, speed
  • 🌈Gradient background — auto-generated with depth
  • Subtle dot pattern — built-in decorative overlay
  • 👆Tap hint — 'Tap to flip' indicator on the front side
  • 📝Sub-text support — add descriptions on both sides
  • 🚀Zero external dependencies — pure Flutter

Perfect for

🃏 Flashcard apps🛍️ Product cards👤 Profile cards❓ FAQ sections💰 Pricing reveals🏆 Portfolio showcases

Integration

How to use it

1

Create a Custom Widget

In your FlutterFlow project, go to Custom Code → Custom Widgets → + Create. Name it exactly `FlipCard`.

2

Add the parameters

Add all 13 parameters from the table below with their correct types and default values.

3

Paste the code & compile

Paste the full widget code from the section below. Hit Save — FlutterFlow will compile it automatically.

4

Drop it into your page

Drag the FlipCard widget onto any page. Set your frontText, backText, colors, and flip direction — done.

API reference

Parameters

ParameterTypeDefaultDescription
widthdouble?300Widget width
heightdouble?200Widget height
frontTextString'Front Side'Main text displayed on the front
backTextString'Back Side'Main text displayed on the back
frontSubTextString''Sub-text on the front (optional)
backSubTextString''Sub-text on the back (optional)
frontBackgroundColorColorDark navyBackground color of the front side
backBackgroundColorColorDark blueBackground color of the back side
frontTextColorColorWhiteText color on the front
backTextColorColorWhiteText color on the back
borderRadiusdouble16.0Card corner radius
flipDurationMsint600Flip animation duration in milliseconds
flipDirectionString'horizontal'Flip axis — 'horizontal' or 'vertical'

Ready to copy

Preset examples

Flashcard / Quiz

dart
FlipCard(
  frontText: "What is Flutter?",
  backText: "A UI toolkit by Google",
  flipDurationMs: 500,
  flipDirection: "horizontal",
)

Product Card

dart
FlipCard(
  frontText: "Nike Air Max",
  frontSubText: "Tap to see details",
  backText: "€129.99",
  backSubText: "Free shipping · 2-day delivery",
  borderRadius: 20,
)

FAQ Section

dart
FlipCard(
  frontText: "How long does setup take?",
  backText: "Under 5 minutes",
  flipDurationMs: 400,
  flipDirection: "vertical",
)

Dramatic Reveal

dart
FlipCard(
  frontText: "Your result is...",
  backText: "🎉 You passed!",
  flipDurationMs: 900,
  borderRadius: 24,
)

Full source

Full widget code

Copy the entire file into FlutterFlow → Custom Code → Custom Widgets.

dart
// Automatic FlutterFlow imports
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
import '/custom_code/widgets/index.dart'; // Imports other custom widgets
import '/flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom widget code
// DO NOT REMOVE OR MODIFY THE CODE ABOVE!

import 'dart:math' show pi;

class FlipCard extends StatefulWidget {
  const FlipCard({
    super.key,
    this.width,
    this.height,
    this.frontText = 'Front Side',
    this.backText = 'Back Side',
    this.frontBackgroundColor = const Color(0xFF1A1A2E),
    this.backBackgroundColor = const Color(0xFF16213E),
    this.frontTextColor = Colors.white,
    this.backTextColor = Colors.white,
    this.borderRadius = 16.0,
    this.flipDurationMs = 600,
    this.frontSubText = '',
    this.backSubText = '',
    this.flipDirection = 'horizontal',
  });

  final double? width;
  final double? height;
  final String frontText;
  final String backText;
  final Color frontBackgroundColor;
  final Color backBackgroundColor;
  final Color frontTextColor;
  final Color backTextColor;
  final double borderRadius;
  final int flipDurationMs;
  final String frontSubText;
  final String backSubText;
  final String flipDirection;

  @override
  State<FlipCard> createState() => _FlipCardState();
}

class _FlipCardState extends State<FlipCard>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;
  bool _isFront = true;
  bool _isAnimating = false;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: widget.flipDurationMs),
    );
    _animation = Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOutBack),
    );

    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed ||
          status == AnimationStatus.dismissed) {
        setState(() {
          _isAnimating = false;
        });
      }
    });
  }

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

  void _toggleCard() {
    if (_isAnimating) return;
    _isAnimating = true;
    if (_isFront) {
      _controller.forward();
    } else {
      _controller.reverse();
    }
    setState(() {
      _isFront = !_isFront;
    });
  }

  @override
  Widget build(BuildContext context) {
    final isHorizontal = widget.flipDirection.toLowerCase() == 'horizontal';

    return GestureDetector(
      onTap: _toggleCard,
      child: AnimatedBuilder(
        animation: _animation,
        builder: (context, child) {
          final angle = _animation.value * pi;
          final isFrontVisible = _animation.value < 0.5;

          Matrix4 transform = Matrix4.identity()
            ..setEntry(3, 2, 0.001);

          if (isHorizontal) {
            transform.rotateY(angle);
          } else {
            transform.rotateX(angle);
          }

          return Transform(
            alignment: Alignment.center,
            transform: transform,
            child: isFrontVisible
                ? _buildFace(
                    text: widget.frontText,
                    subText: widget.frontSubText,
                    backgroundColor: widget.frontBackgroundColor,
                    textColor: widget.frontTextColor,
                    showHint: true,
                  )
                : Transform(
                    alignment: Alignment.center,
                    transform: isHorizontal
                        ? (Matrix4.identity()..rotateY(pi))
                        : (Matrix4.identity()..rotateX(pi)),
                    child: _buildFace(
                      text: widget.backText,
                      subText: widget.backSubText,
                      backgroundColor: widget.backBackgroundColor,
                      textColor: widget.backTextColor,
                      showHint: false,
                    ),
                  ),
          );
        },
      ),
    );
  }

  Widget _buildFace({
    required String text,
    required String subText,
    required Color backgroundColor,
    required Color textColor,
    bool showHint = false,
  }) {
    return Container(
      width: widget.width ?? 300,
      height: widget.height ?? 200,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(widget.borderRadius),
        gradient: LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: [
            backgroundColor,
            Color.lerp(backgroundColor, Colors.black, 0.3) ?? backgroundColor,
          ],
        ),
        boxShadow: [
          BoxShadow(
            color: backgroundColor.withOpacity(0.3),
            blurRadius: 20,
            offset: const Offset(0, 10),
          ),
          BoxShadow(
            color: Colors.black.withOpacity(0.2),
            blurRadius: 10,
            offset: const Offset(0, 5),
          ),
        ],
      ),
      child: Stack(
        children: [
          Positioned.fill(
            child: ClipRRect(
              borderRadius: BorderRadius.circular(widget.borderRadius),
              child: CustomPaint(
                painter: _DotPatternPainter(
                  color: Colors.white.withOpacity(0.03),
                ),
              ),
            ),
          ),
          Center(
            child: Padding(
              padding: const EdgeInsets.all(24.0),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(
                    text,
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      color: textColor,
                      fontSize: 22,
                      fontWeight: FontWeight.bold,
                      letterSpacing: 0.5,
                    ),
                  ),
                  if (subText.isNotEmpty) ...[
                    const SizedBox(height: 8),
                    Text(
                      subText,
                      textAlign: TextAlign.center,
                      style: TextStyle(
                        color: textColor.withOpacity(0.7),
                        fontSize: 14,
                        fontWeight: FontWeight.w400,
                      ),
                    ),
                  ],
                ],
              ),
            ),
          ),
          if (showHint)
            Positioned(
              bottom: 12,
              left: 0,
              right: 0,
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(
                    Icons.touch_app_rounded,
                    color: textColor.withOpacity(0.3),
                    size: 16,
                  ),
                  const SizedBox(width: 4),
                  Text(
                    'Tap to flip',
                    style: TextStyle(
                      color: textColor.withOpacity(0.3),
                      fontSize: 12,
                    ),
                  ),
                ],
              ),
            ),
        ],
      ),
    );
  }
}

class _DotPatternPainter extends CustomPainter {
  final Color color;
  _DotPatternPainter({required this.color});

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = color
      ..style = PaintingStyle.fill;
    const spacing = 20.0;
    for (double x = 0; x < size.width; x += spacing) {
      for (double y = 0; y < size.height; y += spacing) {
        canvas.drawCircle(Offset(x, y), 1, paint);
      }
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

MIT — Free to use in personal and commercial projects.

From the trenches

Pro tips

Contrasting colors:: Use clearly different colors for front and back — the flip is more satisfying when the reveal feels distinct.

Snappy vs dramatic:: Try `flipDurationMs: 400` for a quick snappy feel or `800` for a slow dramatic reveal.

Keep text short:: The card surface is limited — use sub-text for details, keep the main label punchy.

FAQ rows:: Put multiple FlipCards in a Column with equal widths to build a clean accordion-style FAQ section.

Building an app?

Need a full MVP, not just widgets?

I build complete apps for founders — fixed prices, fast delivery. Book a free 30-minute call and let's talk about your idea.

Book a free call