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.
Interactive demo
Try it yourself ↓
What's included
Perfect for
Integration
In your FlutterFlow project, go to Custom Code → Custom Widgets → + Create. Name it exactly `FlipCard`.
Add all 13 parameters from the table below with their correct types and default values.
Paste the full widget code from the section below. Hit Save — FlutterFlow will compile it automatically.
Drag the FlipCard widget onto any page. Set your frontText, backText, colors, and flip direction — done.
API reference
| Parameter | Type | Default | Description |
|---|---|---|---|
| width | double? | 300 | Widget width |
| height | double? | 200 | Widget height |
| frontText | String | 'Front Side' | Main text displayed on the front |
| backText | String | 'Back Side' | Main text displayed on the back |
| frontSubText | String | '' | Sub-text on the front (optional) |
| backSubText | String | '' | Sub-text on the back (optional) |
| frontBackgroundColor | Color | Dark navy | Background color of the front side |
| backBackgroundColor | Color | Dark blue | Background color of the back side |
| frontTextColor | Color | White | Text color on the front |
| backTextColor | Color | White | Text color on the back |
| borderRadius | double | 16.0 | Card corner radius |
| flipDurationMs | int | 600 | Flip animation duration in milliseconds |
| flipDirection | String | 'horizontal' | Flip axis — 'horizontal' or 'vertical' |
Ready to copy
FlipCard(
frontText: "What is Flutter?",
backText: "A UI toolkit by Google",
flipDurationMs: 500,
flipDirection: "horizontal",
)FlipCard(
frontText: "Nike Air Max",
frontSubText: "Tap to see details",
backText: "€129.99",
backSubText: "Free shipping · 2-day delivery",
borderRadius: 20,
)FlipCard(
frontText: "How long does setup take?",
backText: "Under 5 minutes",
flipDurationMs: 400,
flipDirection: "vertical",
)FlipCard(
frontText: "Your result is...",
backText: "🎉 You passed!",
flipDurationMs: 900,
borderRadius: 24,
)Full source
Copy the entire file into FlutterFlow → Custom Code → Custom Widgets.
// 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
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.
Free & open source
Drop-in shimmer loading animation. Five layout presets, fully customizable.
Smart text truncation with '...' append. Zero dependencies, pure Dart.
Customizable dotted and dashed borders. Because FlutterFlow doesn't have this built in.
Profile image with initials fallback. Same name always gets the same color.
Building an app?
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