image

02 Apr 2026

9K

35K

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

In modern e-commerce applications, user experience is paramount. A "Product Quick View" feature significantly enhances this by allowing users to get essential product information, see related items, and spot promotions without navigating away from the current product listing page. This minimizes clicks and streamlines the shopping process, often leading to better conversion rates. In Flutter, we can create a sophisticated Quick View widget with animations and rich content.

This article will guide you through building such a widget, incorporating a smooth slide-up animation, displaying related products, and highlighting promotions with a badge.

1. Core Structure of the Quick View Widget

We'll implement the Quick View as a modal bottom sheet, which slides up from the bottom of the screen. This is a common and user-friendly pattern in mobile applications. The content within this bottom sheet will be a custom Flutter widget.


import 'package:flutter/material.dart';

// Dummy Product Model
class Product {
  final String id;
  final String name;
  final String imageUrl;
  final double price;
  final double? salePrice;
  final String description;
  final List relatedProductIds;

  Product({
    required this.id,
    required this.name,
    required this.imageUrl,
    required this.price,
    this.salePrice,
    required this.description,
    this.relatedProductIds = const [],
  });

  bool get isOnSale => salePrice != null && salePrice! < price;
}

// Dummy Data Service
class ProductService {
  static final List _products = [
    Product(
      id: 'p1',
      name: 'Stylish Running Shoes',
      imageUrl: 'https://picsum.photos/id/20/400/400',
      price: 99.99,
      salePrice: 79.99,
      description: 'Comfortable and stylish running shoes for your daily run.',
      relatedProductIds: ['p2', 'p3'],
    ),
    Product(
      id: 'p2',
      name: 'Ergonomic Backpack',
      imageUrl: 'https://picsum.photos/id/30/400/400',
      price: 59.99,
      description: 'Durable backpack with ergonomic design for everyday use.',
      relatedProductIds: ['p1', 'p4'],
    ),
    Product(
      id: 'p3',
      name: 'Water Bottle (500ml)',
      imageUrl: 'https://picsum.photos/id/40/400/400',
      price: 19.99,
      salePrice: 14.99,
      description: 'Leak-proof water bottle, perfect for sports and travel.',
      relatedProductIds: ['p1', 'p2'],
    ),
    Product(
      id: 'p4',
      name: 'Wireless Earbuds',
      imageUrl: 'https://picsum.photos/id/50/400/400',
      price: 129.99,
      description: 'High-quality wireless earbuds with noise cancellation.',
      relatedProductIds: ['p2', 'p3'],
    ),
  ];

  static Product? getProductById(String id) {
    return _products.firstWhere((p) => p.id == id, orElse: () => throw Exception('Product not found'));
  }

  static List getRelatedProducts(List ids) {
    return ids.map((id) => getProductById(id)!).toList();
  }

  static List getAllProducts() {
    return _products;
  }
}

// The main widget to display a list of products
class ProductListingScreen extends StatelessWidget {
  const ProductListingScreen({super.key});

  void _showQuickView(BuildContext context, Product product) {
    showModalBottomSheet(
      context: context,
      isScrollControlled: true, // Allow the sheet to be full height if needed
      backgroundColor: Colors.transparent, // To show rounded corners on content
      builder: (ctx) {
        return ProductQuickView(product: product);
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    final products = ProductService.getAllProducts();
    return Scaffold(
      appBar: AppBar(title: const Text('Product Listings')),
      body: ListView.builder(
        itemCount: products.length,
        itemBuilder: (context, index) {
          final product = products[index];
          return Card(
            margin: const EdgeInsets.all(8.0),
            child: ListTile(
              leading: Image.network(product.imageUrl, width: 50, height: 50, fit: BoxFit.cover),
              title: Text(product.name),
              subtitle: Text('\$${product.price.toStringAsFixed(2)}'),
              trailing: ElevatedButton(
                onPressed: () => _showQuickView(context, product),
                child: const Text('Quick View'),
              ),
            ),
          );
        },
      ),
    );
  }
}

2. Implementing Animation (Slide-Up/Fade-In)

For a smooth user experience, we'll add a slide-up animation to the Quick View content as it appears. We'll use an AnimationController and a SlideTransition widget.


import 'package:flutter/material.dart';

// (Product and ProductService models from previous section)
// ...

class ProductQuickView extends StatefulWidget {
  final Product product;

  const ProductQuickView({super.key, required this.product});

  @override
  State createState() => _ProductQuickViewState();
}

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

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );

    _slideAnimation = Tween(
      begin: const Offset(0, 1), // Start from below the screen
      end: Offset.zero, // End at its normal position
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeOutCubic,
    ));

    _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(
        parent: _animationController,
        curve: Curves.easeIn,
      ),
    );

    _animationController.forward(); // Start the animation when the widget loads
  }

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

  @override
  Widget build(BuildContext context) {
    return SlideTransition(
      position: _slideAnimation,
      child: FadeTransition(
        opacity: _fadeAnimation,
        child: Container(
          decoration: const BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
          ),
          // We'll add the content here in the next steps
          child: Column(
            mainAxisSize: MainAxisSize.min, // Essential for bottom sheet
            children: [
              // Drag handle
              Container(
                height: 4,
                width: 40,
                margin: const EdgeInsets.symmetric(vertical: 10),
                decoration: BoxDecoration(
                  color: Colors.grey[300],
                  borderRadius: BorderRadius.circular(2),
                ),
              ),
              // Rest of the content will go here
              const Padding(
                padding: EdgeInsets.all(16.0),
                child: Text('Product Quick View Content'),
              ),
              SizedBox(height: MediaQuery.of(context).padding.bottom), // safe area for bottom sheet
            ],
          ),
        ),
      ),
    );
  }
}

3. Displaying Product Details and Promo Badge

Now, let's populate the Quick View with the product's image, name, price, and a special badge for promotional offers.


import 'package:flutter/material.dart';

// (Product and ProductService models from previous section)
// ...

class ProductQuickView extends StatefulWidget {
  final Product product;

  const ProductQuickView({super.key, required this.product});

  @override
  State createState() => _ProductQuickViewState();
}

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

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );

    _slideAnimation = Tween(
      begin: const Offset(0, 1),
      end: Offset.zero,
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeOutCubic,
    ));

    _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 SlideTransition(
      position: _slideAnimation,
      child: FadeTransition(
        opacity: _fadeAnimation,
        child: Container(
          decoration: const BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
          ),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // Drag handle
              Center(
                child: Container(
                  height: 4,
                  width: 40,
                  margin: const EdgeInsets.symmetric(vertical: 10),
                  decoration: BoxDecoration(
                    color: Colors.grey[300],
                    borderRadius: BorderRadius.circular(2),
                  ),
                ),
              ),
              // Product Image and Promo Badge
              Stack(
                alignment: Alignment.topRight,
                children: [
                  ClipRRect(
                    borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
                    child: Image.network(
                      widget.product.imageUrl,
                      height: 200,
                      width: double.infinity,
                      fit: BoxFit.cover,
                    ),
                  ),
                  if (widget.product.isOnSale)
                    Positioned(
                      top: 10,
                      right: 10,
                      child: Container(
                        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                        decoration: BoxDecoration(
                          color: Colors.redAccent,
                          borderRadius: BorderRadius.circular(5),
                        ),
                        child: Text(
                          '-${(((widget.product.price - widget.product.salePrice!) / widget.product.price) * 100).toStringAsFixed(0)}%',
                          style: const TextStyle(
                            color: Colors.white,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                      ),
                    ),
                ],
              ),
              // Product Details
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      widget.product.name,
                      style: const TextStyle(
                        fontSize: 22,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 8),
                    Row(
                      children: [
                        Text(
                          '\$${widget.product.salePrice?.toStringAsFixed(2) ?? widget.product.price.toStringAsFixed(2)}',
                          style: TextStyle(
                            fontSize: 18,
                            fontWeight: FontWeight.bold,
                            color: widget.product.isOnSale ? Colors.red : Colors.green,
                          ),
                        ),
                        if (widget.product.isOnSale)
                          Padding(
                            padding: const EdgeInsets.only(left: 8.0),
                            child: Text(
                              '\$${widget.product.price.toStringAsFixed(2)}',
                              style: const TextStyle(
                                fontSize: 16,
                                color: Colors.grey,
                                decoration: TextDecoration.lineThrough,
                              ),
                            ),
                          ),
                      ],
                    ),
                    const SizedBox(height: 12),
                    Text(
                      widget.product.description,
                      maxLines: 3,
                      overflow: TextOverflow.ellipsis,
                      style: const TextStyle(fontSize: 14, color: Colors.grey),
                    ),
                    const SizedBox(height: 16),
                    SizedBox(
                      width: double.infinity,
                      child: ElevatedButton.icon(
                        onPressed: () {
                          // Handle add to cart or view full details
                          Navigator.of(context).pop(); // Close quick view
                          ScaffoldMessenger.of(context).showSnackBar(
                            SnackBar(content: Text('${widget.product.name} added to cart!')),
                          );
                        },
                        icon: const Icon(Icons.shopping_cart),
                        label: const Text('Add to Cart'),
                        style: ElevatedButton.styleFrom(
                          padding: const EdgeInsets.symmetric(vertical: 12),
                          shape: RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(8),
                          ),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
              // Related Items will be added here
              SizedBox(height: MediaQuery.of(context).padding.bottom),
            ],
          ),
        ),
      ),
    );
  }
}

4. Adding Related Items

To encourage further exploration, we'll add a horizontal scrollable list of related products at the bottom of the Quick View. This will make use of our ProductService to fetch related items.


import 'package:flutter/material.dart';

// (Product and ProductService models from previous section)
// ...

class ProductQuickView extends StatefulWidget {
  final Product product;

  const ProductQuickView({super.key, required this.product});

  @override
  State createState() => _ProductQuickViewState();
}

class _ProductQuickViewState extends State
    with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation _slideAnimation;
  late Animation _fadeAnimation;
  List _relatedProducts = [];

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );

    _slideAnimation = Tween(
      begin: const Offset(0, 1),
      end: Offset.zero,
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeOutCubic,
    ));

    _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(
        parent: _animationController,
        curve: Curves.easeIn,
      ),
    );

    _animationController.forward();
    _fetchRelatedProducts();
  }

  void _fetchRelatedProducts() {
    setState(() {
      _relatedProducts = ProductService.getRelatedProducts(widget.product.relatedProductIds);
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return SlideTransition(
      position: _slideAnimation,
      child: FadeTransition(
        opacity: _fadeAnimation,
        child: Container(
          decoration: const BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
          ),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // Drag handle
              Center(
                child: Container(
                  height: 4,
                  width: 40,
                  margin: const EdgeInsets.symmetric(vertical: 10),
                  decoration: BoxDecoration(
                    color: Colors.grey[300],
                    borderRadius: BorderRadius.circular(2),
                  ),
                ),
              ),
              // Product Image and Promo Badge
              Stack(
                alignment: Alignment.topRight,
                children: [
                  ClipRRect(
                    borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
                    child: Image.network(
                      widget.product.imageUrl,
                      height: 200,
                      width: double.infinity,
                      fit: BoxFit.cover,
                    ),
                  ),
                  if (widget.product.isOnSale)
                    Positioned(
                      top: 10,
                      right: 10,
                      child: Container(
                        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                        decoration: BoxDecoration(
                          color: Colors.redAccent,
                          borderRadius: BorderRadius.circular(5),
                        ),
                        child: Text(
                          '-${(((widget.product.price - widget.product.salePrice!) / widget.product.price) * 100).toStringAsFixed(0)}%',
                          style: const TextStyle(
                            color: Colors.white,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                      ),
                    ),
                ],
              ),
              // Product Details
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      widget.product.name,
                      style: const TextStyle(
                        fontSize: 22,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 8),
                    Row(
                      children: [
                        Text(
                          '\$${widget.product.salePrice?.toStringAsFixed(2) ?? widget.product.price.toStringAsFixed(2)}',
                          style: TextStyle(
                            fontSize: 18,
                            fontWeight: FontWeight.bold,
                            color: widget.product.isOnSale ? Colors.red : Colors.green,
                          ),
                        ),
                        if (widget.product.isOnSale)
                          Padding(
                            padding: const EdgeInsets.only(left: 8.0),
                            child: Text(
                              '\$${widget.product.price.toStringAsFixed(2)}',
                              style: const TextStyle(
                                fontSize: 16,
                                color: Colors.grey,
                                decoration: TextDecoration.lineThrough,
                              ),
                            ),
                          ),
                      ],
                    ),
                    const SizedBox(height: 12),
                    Text(
                      widget.product.description,
                      maxLines: 3,
                      overflow: TextOverflow.ellipsis,
                      style: const TextStyle(fontSize: 14, color: Colors.grey),
                    ),
                    const SizedBox(height: 16),
                    SizedBox(
                      width: double.infinity,
                      child: ElevatedButton.icon(
                        onPressed: () {
                          Navigator.of(context).pop();
                          ScaffoldMessenger.of(context).showSnackBar(
                            SnackBar(content: Text('${widget.product.name} added to cart!')),
                          );
                        },
                        icon: const Icon(Icons.shopping_cart),
                        label: const Text('Add to Cart'),
                        style: ElevatedButton.styleFrom(
                          padding: const EdgeInsets.symmetric(vertical: 12),
                          shape: RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(8),
                          ),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
              // Related Items Section
              if (_relatedProducts.isNotEmpty)
                Padding(
                  padding: const EdgeInsets.symmetric(vertical: 8.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      const Padding(
                        padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
                        child: Text(
                          'Related Items',
                          style: TextStyle(
                            fontSize: 18,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                      ),
                      SizedBox(
                        height: 160, // Height for the horizontal list
                        child: ListView.builder(
                          scrollDirection: Axis.horizontal,
                          padding: const EdgeInsets.symmetric(horizontal: 16.0),
                          itemCount: _relatedProducts.length,
                          itemBuilder: (context, index) {
                            final relatedProduct = _relatedProducts[index];
                            return GestureDetector(
                              onTap: () {
                                // Close current quick view and open new one for related product
                                Navigator.of(context).pop();
                                showModalBottomSheet(
                                  context: context,
                                  isScrollControlled: true,
                                  backgroundColor: Colors.transparent,
                                  builder: (ctx) {
                                    return ProductQuickView(product: relatedProduct);
                                  },
                                );
                              },
                              child: Container(
                                width: 120,
                                margin: const EdgeInsets.only(right: 12),
                                decoration: BoxDecoration(
                                  color: Colors.grey[100],
                                  borderRadius: BorderRadius.circular(8),
                                ),
                                child: Column(
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  children: [
                                    ClipRRect(
                                      borderRadius: const BorderRadius.vertical(
                                        top: Radius.circular(8),
                                      ),
                                      child: Image.network(
                                        relatedProduct.imageUrl,
                                        height: 80,
                                        width: double.infinity,
                                        fit: BoxFit.cover,
                                      ),
                                    ),
                                    Padding(
                                      padding: const EdgeInsets.all(8.0),
                                      child: Column(
                                        crossAxisAlignment: CrossAxisAlignment.start,
                                        children: [
                                          Text(
                                            relatedProduct.name,
                                            maxLines: 2,
                                            overflow: TextOverflow.ellipsis,
                                            style: const TextStyle(
                                              fontSize: 12,
                                              fontWeight: FontWeight.w600,
                                            ),
                                          ),
                                          const SizedBox(height: 4),
                                          Text(
                                            '\$${relatedProduct.salePrice?.toStringAsFixed(2) ?? relatedProduct.price.toStringAsFixed(2)}',
                                            style: TextStyle(
                                              fontSize: 12,
                                              fontWeight: FontWeight.bold,
                                              color: relatedProduct.isOnSale ? Colors.red : Colors.green,
                                            ),
                                          ),
                                        ],
                                      ),
                                    ),
                                  ],
                                ),
                              ),
                            );
                          },
                        ),
                      ),
                    ],
                  ),
                ),
              SizedBox(height: MediaQuery.of(context).padding.bottom),
            ],
          ),
        ),
      ),
    );
  }
}

Conclusion

A well-designed Product Quick View widget significantly enhances the user experience in e-commerce applications. By incorporating smooth animations, clear product details, appealing promo badges, and convenient access to related items, you can provide users with a rich and efficient shopping journey without constant page navigations.

This article demonstrated how to build such a widget in Flutter, utilizing showModalBottomSheet for its structure, AnimationController and SlideTransition for a polished entry animation, a Stack and Positioned for the promo badge, and a horizontal ListView.builder for displaying related products. Remember to tailor the styling and interaction to match your application's specific design guidelines.

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