image

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

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