Building a Product Quick View Widget with Animation and Promo Badge in Flutter

27 Apr 2026

9K

35K

Building a Product Quick View Widget with Animation and Promo Badge in Flutter

In the dynamic world of e-commerce, user experience is paramount. A "Product Quick View" widget significantly enhances this experience by allowing users to get essential product information at a glance, without navigating to a separate product detail page. When combined with subtle animations and eye-catching promo badges, these widgets can drive engagement and conversions. This article will guide you through building such a sophisticated widget in Flutter.

What is a Product Quick View Widget?

A Product Quick View widget is a compact UI element designed to display key details of a product, such as its image, name, price, and sometimes a brief description or rating, directly within a product listing or category page. Its primary goal is to provide immediate information, reducing friction in the browsing process.

Why Incorporate Animation and Promo Badges?

  • Animation: Smooth entry animations (e.g., fade-in, slide-up) make the user interface feel more responsive, modern, and delightful. They guide the user's eye and improve the overall aesthetic.
  • Promo Badge: A visual indicator like "Sale," "New," "Limited Offer," or a discount percentage immediately highlights special deals, grabs attention, and can create a sense of urgency, encouraging users to explore further or make a purchase.

Step-by-Step Implementation in Flutter

1. Define the Product Model

First, let's create a simple data model for our product.


class Product {
  final String id;
  final String name;
  final String imageUrl;
  final double price;
  final double? salePrice;
  final String? promoBadge;

  Product({
    required this.id,
    required this.name,
    required this.imageUrl,
    required this.price,
    this.salePrice,
    this.promoBadge,
  });
}

2. Basic Product Quick View Widget Structure

We'll start with a basic `ProductQuickView` widget that displays the product's image, name, and price.


import 'package:flutter/material.dart';
// Assuming Product class is in 'product.dart' or defined above

class ProductQuickView extends StatefulWidget {
  final Product product;
  final VoidCallback? onTap;

  const ProductQuickView({
    Key? key,
    required this.product,
    this.onTap,
  }) : super(key: key);

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

class _ProductQuickViewWidgetState extends State {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: widget.onTap,
      child: Card(
        elevation: 2,
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // Product Image
              Center(
                child: Image.network(
                  widget.product.imageUrl,
                  height: 120,
                  width: double.infinity,
                  fit: BoxFit.cover,
                  errorBuilder: (context, error, stackTrace) => Container(
                    height: 120,
                    color: Colors.grey[200],
                    child: const Icon(Icons.broken_image, size: 50, color: Colors.grey),
                  ),
                ),
              ),
              const SizedBox(height: 8),
              // Product Name
              Text(
                widget.product.name,
                maxLines: 2,
                overflow: TextOverflow.ellipsis,
                style: const TextStyle(
                  fontWeight: FontWeight.bold,
                  fontSize: 14,
                ),
              ),
              const SizedBox(height: 4),
              // Product Price
              Row(
                children: [
                  if (widget.product.salePrice != null)
                    Text(
                      '\$${widget.product.price.toStringAsFixed(2)}',
                      style: const TextStyle(
                        decoration: TextDecoration.lineThrough,
                        color: Colors.grey,
                        fontSize: 12,
                      ),
                    ),
                  if (widget.product.salePrice != null) const SizedBox(width: 4),
                  Text(
                    '\$${(widget.product.salePrice ?? widget.product.price).toStringAsFixed(2)}',
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                      fontSize: 16,
                      color: widget.product.salePrice != null ? Colors.red : Colors.black,
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

3. Adding Animation (Fade-in)

We'll use an `AnimationController` and `FadeTransition` to create a smooth fade-in effect when the widget appears.


import 'package:flutter/material.dart';
// Assuming Product class is in 'product.dart' or defined above

class ProductQuickView extends StatefulWidget {
  final Product product;
  final VoidCallback? onTap;

  const ProductQuickView({
    Key? key,
    required this.product,
    this.onTap,
  }) : super(key: key);

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

class _ProductQuickViewWidgetState extends State
    with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation _fadeAnimation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 500),
    );
    _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(parent: _animationController, curve: Curves.easeIn),
    );

    _animationController.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return FadeTransition( // Wrap with FadeTransition
      opacity: _fadeAnimation,
      child: GestureDetector(
        onTap: widget.onTap,
        child: Card(
          elevation: 2,
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                // Product Image
                Center(
                  child: Image.network(
                    widget.product.imageUrl,
                    height: 120,
                    width: double.infinity,
                    fit: BoxFit.cover,
                    errorBuilder: (context, error, stackTrace) => Container(
                      height: 120,
                      color: Colors.grey[200],
                      child: const Icon(Icons.broken_image, size: 50, color: Colors.grey),
                    ),
                  ),
                ),
                const SizedBox(height: 8),
                // Product Name
                Text(
                  widget.product.name,
                  maxLines: 2,
                  overflow: TextOverflow.ellipsis,
                  style: const TextStyle(
                    fontWeight: FontWeight.bold,
                    fontSize: 14,
                  ),
                ),
                const SizedBox(height: 4),
                // Product Price
                Row(
                  children: [
                    if (widget.product.salePrice != null)
                      Text(
                        '\$${widget.product.price.toStringAsFixed(2)}',
                        style: const TextStyle(
                          decoration: TextDecoration.lineThrough,
                          color: Colors.grey,
                          fontSize: 12,
                        ),
                      ),
                    if (widget.product.salePrice != null) const SizedBox(width: 4),
                    Text(
                      '\$${(widget.product.salePrice ?? widget.product.price).toStringAsFixed(2)}',
                      style: TextStyle(
                        fontWeight: FontWeight.bold,
                        fontSize: 16,
                        color: widget.product.salePrice != null ? Colors.red : Colors.black,
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

4. Implementing the Promo Badge

To add a promo badge, we'll wrap the main content in a `Stack` widget. This allows us to layer the badge on top of the product image using `Positioned`.


import 'package:flutter/material.dart';
// Assuming Product class is in 'product.dart' or defined above

class ProductQuickView extends StatefulWidget {
  final Product product;
  final VoidCallback? onTap;

  const ProductQuickView({
    Key? key,
    required this.product,
    this.onTap,
  }) : super(key: key);

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

class _ProductQuickViewWidgetState extends State
    with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation _fadeAnimation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 500),
    );
    _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(parent: _animationController, curve: Curves.easeIn),
    );

    _animationController.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return FadeTransition(
      opacity: _fadeAnimation,
      child: GestureDetector(
        onTap: widget.onTap,
        child: Card(
          elevation: 2,
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
          child: Column( // Column to hold the Stack and other content
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Stack( // Stack for image and promo badge
                children: [
                  ClipRRect(
                    borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
                    child: Image.network(
                      widget.product.imageUrl,
                      height: 120,
                      width: double.infinity,
                      fit: BoxFit.cover,
                      errorBuilder: (context, error, stackTrace) => Container(
                        height: 120,
                        color: Colors.grey[200],
                        child: const Icon(Icons.broken_image, size: 50, color: Colors.grey),
                      ),
                    ),
                  ),
                  if (widget.product.promoBadge != null)
                    Positioned(
                      top: 8,
                      left: 8,
                      child: Container(
                        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                        decoration: BoxDecoration(
                          color: Colors.redAccent,
                          borderRadius: BorderRadius.circular(4),
                        ),
                        child: Text(
                          widget.product.promoBadge!,
                          style: const TextStyle(
                            color: Colors.white,
                            fontSize: 10,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                      ),
                    ),
                ],
              ),
              Padding(
                padding: const EdgeInsets.all(8.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const SizedBox(height: 8), // Re-added spacing after image area
                    // Product Name
                    Text(
                      widget.product.name,
                      maxLines: 2,
                      overflow: TextOverflow.ellipsis,
                      style: const TextStyle(
                        fontWeight: FontWeight.bold,
                        fontSize: 14,
                      ),
                    ),
                    const SizedBox(height: 4),
                    // Product Price
                    Row(
                      children: [
                        if (widget.product.salePrice != null)
                          Text(
                            '\$${widget.product.price.toStringAsFixed(2)}',
                            style: const TextStyle(
                              decoration: TextDecoration.lineThrough,
                              color: Colors.grey,
                              fontSize: 12,
                            ),
                          ),
                        if (widget.product.salePrice != null) const SizedBox(width: 4),
                        Text(
                          '\$${(widget.product.salePrice ?? widget.product.price).toStringAsFixed(2)}',
                          style: TextStyle(
                            fontWeight: FontWeight.bold,
                            fontSize: 16,
                            color: widget.product.salePrice != null ? Colors.red : Colors.black,
                          ),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

5. Usage Example

Here's how you might use these `ProductQuickView` widgets in a `GridView` on your home page.


import 'package:flutter/material.dart';
// Import your Product and ProductQuickView widgets
// import 'product_quick_view.dart';
// import 'product.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter E-commerce Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const ProductListingPage(),
    );
  }
}

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

  final List<Product> products = const [
    Product(
      id: '1',
      name: 'Stylish Running Shoes',
      imageUrl: 'https://via.placeholder.com/150/FF0000/FFFFFF?text=Running+Shoes',
      price: 89.99,
      salePrice: 79.99,
      promoBadge: 'SALE!',
    ),
    Product(
      id: '2',
      name: 'Classic Leather Wallet',
      imageUrl: 'https://via.placeholder.com/150/0000FF/FFFFFF?text=Leather+Wallet',
      price: 45.00,
      promoBadge: 'NEW',
    ),
    Product(
      id: '3',
      name: 'Wireless Bluetooth Headphones with Noise Cancellation',
      imageUrl: 'https://via.placeholder.com/150/008000/FFFFFF?text=Headphones',
      price: 120.00,
    ),
    Product(
      id: '4',
      name: 'Designer Sunglasses UV400 Protection',
      imageUrl: 'https://via.placeholder.com/150/FFFF00/000000?text=Sunglasses',
      price: 65.50,
      salePrice: 55.00,
      promoBadge: '-15%',
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Product Listings'),
      ),
      body: GridView.builder(
        padding: const EdgeInsets.all(16.0),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2, // Two items per row
          crossAxisSpacing: 16.0,
          mainAxisSpacing: 16.0,
          childAspectRatio: 0.75, // Adjust as needed
        ),
        itemCount: products.length,
        itemBuilder: (context, index) {
          final product = products[index];
          return ProductQuickView(
            product: product,
            onTap: () {
              // Handle tap, e.g., navigate to product detail page
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(content: Text('Tapped on ${product.name}')),
              );
            },
          );
        },
      ),
    );
  }
}

Conclusion

By following these steps, you can create a highly engaging and informative Product Quick View widget in Flutter. The combination of a clear display, a subtle fade-in animation, and a prominent promo badge makes for a compelling user interface component that can significantly improve the browsing and shopping experience in your e-commerce application. Further enhancements could include different animation types, customizable badge styles, or integrating add-to-cart functionality directly into the quick view.

Related Articles

Aug 02, 2026

Building a Flutter Notification Center with Grouping & Actions

Learn to create a sophisticated in-app notification center in Flutter, featuring grouping, swipe-to-dismiss, custom icons, and actionable buttons for enhanced u

Aug 01, 2026

Flutter Layout Tips: Using FractionallySizedBox, AspectRatio, ConstrainedBox for Adaptive UI

Master Flutter's FractionallySizedBox, AspectRatio, and ConstrainedBox to build truly adaptive UIs. Learn how to control widget sizing, maintain proportions, an

Aug 01, 2026

Flutter Product Quick View: Animation, Related Items, Badges

Learn to create a dynamic product quick view widget in Flutter, incorporating smooth animations, relevant related items, and eye-catching promo badges for an en