image

20 Mar 2026

9K

35K

Creating a Portfolio Carousel Widget with Image Overlay and Caption in Flutter

Flutter offers powerful UI tools to build engaging and dynamic user interfaces. A common requirement for portfolio websites or apps is a carousel to showcase projects, images, or products. This article will guide you through creating a sophisticated portfolio carousel widget in Flutter, complete with image display, an elegant overlay, and descriptive captions.

Our goal is to build a carousel where each slide features an image, and on top of that image, there's a semi-transparent overlay containing a title and a description. Users will be able to swipe through these portfolio items seamlessly.

Core Concepts and Flutter Widgets

To achieve this, we'll leverage several key Flutter widgets:

  • PageView: The foundation of our carousel, allowing users to swipe horizontally between pages.
  • Stack: Essential for layering widgets. We'll use it to place the image at the bottom, and the overlay and text on top.
  • Positioned: Used within a Stack to precisely control the placement of its children.
  • Container: For creating the background overlay with styling (color, opacity, padding).
  • Image.asset or Image.network: To display local or network images.
  • Text: To render the title and caption.
  • PageController: To programmatically control the PageView and observe page changes.

Step-by-Step Implementation

Let's break down the process into manageable steps.

1. Define the Data Model

First, we need a simple data structure to represent each portfolio item. This will hold the image path, title, and a brief description.


class PortfolioItem {
  final String imageUrl;
  final String title;
  final String description;

  PortfolioItem({
    required this.imageUrl,
    required this.title,
    required this.description,
  });
}

2. Create the Portfolio Carousel Widget

This will be a StatefulWidget to manage the current page index. It will contain the PageView and the list of portfolio items.


import 'package:flutter/material.dart';

// You would typically define PortfolioItem in a separate file or at the top of this file.
// For this example, let's assume it's available.
// class PortfolioItem { ... }

class PortfolioCarouselWidget extends StatefulWidget {
  final List items;

  const PortfolioCarouselWidget({Key? key, required this.items}) : super(key: key);

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

class _PortfolioCarouselWidgetState extends State {
  late PageController _pageController;
  int _currentPage = 0;

  @override
  void initState() {
    super.initState();
    _pageController = PageController(viewportFraction: 1.0); // Full width items
    _pageController.addListener(() {
      setState(() {
        // Round the page value to get the current integer page index
        // This is useful if viewportFraction is less than 1.0, or for precise tracking
        _currentPage = _pageController.page!.round();
      });
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 300, // Fixed height for the carousel, adjust as needed
      child: PageView.builder(
        controller: _pageController,
        itemCount: widget.items.length,
        itemBuilder: (context, index) {
          return _buildPortfolioCard(widget.items[index]);
        },
      ),
    );
  }

  Widget _buildPortfolioCard(PortfolioItem item) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 8.0), // Spacing between cards
      child: ClipRRect(
        borderRadius: BorderRadius.circular(12.0), // Rounded corners for the card
        child: Stack(
          children: [
            // Background Image
            // Using Image.asset assuming local assets. For network images, use Image.network.
            Image.asset(
              item.imageUrl,
              fit: BoxFit.cover,
              width: double.infinity,
              height: double.infinity,
            ),
            // Image Overlay and Caption
            Positioned.fill(
              child: Container(
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topCenter,
                    end: Alignment.bottomCenter,
                    colors: [
                      Colors.transparent,
                      Colors.black.withOpacity(0.7), // Semi-transparent black at the bottom
                    ],
                    stops: const [0.6, 1.0], // Gradient starts at 60% down
                  ),
                ),
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.end, // Align content to the bottom
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        item.title,
                        style: const TextStyle(
                          color: Colors.white,
                          fontSize: 24,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      const SizedBox(height: 8),
                      Text(
                        item.description,
                        style: TextStyle(
                          color: Colors.white.withOpacity(0.8),
                          fontSize: 16,
                        ),
                        maxLines: 2,
                        overflow: TextOverflow.ellipsis,
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

3. Usage Example (Main Application)

To see your carousel in action, you'll need to prepare some images (e.g., in your assets/images folder) and then integrate the widget into your main application structure. Make sure your PortfolioItem class is accessible.


import 'package:flutter/material.dart';
// Assuming PortfolioItem and PortfolioCarouselWidget are in a file named portfolio_carousel_widget.dart
// import 'path_to_your_widget/portfolio_carousel_widget.dart';

// Re-defining PortfolioItem here for a self-contained example.
class PortfolioItem {
  final String imageUrl;
  final String title;
  final String description;

  PortfolioItem({
    required this.imageUrl,
    required this.title,
    required this.description,
  });
}

// Re-defining PortfolioCarouselWidget here for a self-contained example.
// In a real app, these would be imported from separate files.
class PortfolioCarouselWidget extends StatefulWidget {
  final List items;

  const PortfolioCarouselWidget({Key? key, required this.items}) : super(key: key);

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

class _PortfolioCarouselWidgetState extends State {
  late PageController _pageController;
  int _currentPage = 0;

  @override
  void initState() {
    super.initState();
    _pageController = PageController(viewportFraction: 1.0);
    _pageController.addListener(() {
      setState(() {
        _currentPage = _pageController.page!.round();
      });
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 300,
      child: PageView.builder(
        controller: _pageController,
        itemCount: widget.items.length,
        itemBuilder: (context, index) {
          return _buildPortfolioCard(widget.items[index]);
        },
      ),
    );
  }

  Widget _buildPortfolioCard(PortfolioItem item) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 8.0),
      child: ClipRRect(
        borderRadius: BorderRadius.circular(12.0),
        child: Stack(
          children: [
            Image.asset(
              item.imageUrl,
              fit: BoxFit.cover,
              width: double.infinity,
              height: double.infinity,
            ),
            Positioned.fill(
              child: Container(
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topCenter,
                    end: Alignment.bottomCenter,
                    colors: [
                      Colors.transparent,
                      Colors.black.withOpacity(0.7),
                    ],
                    stops: const [0.6, 1.0],
                  ),
                ),
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.end,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        item.title,
                        style: const TextStyle(
                          color: Colors.white,
                          fontSize: 24,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      const SizedBox(height: 8),
                      Text(
                        item.description,
                        style: TextStyle(
                          color: Colors.white.withOpacity(0.8),
                          fontSize: 16,
                        ),
                        maxLines: 2,
                        overflow: TextOverflow.ellipsis,
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}


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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Portfolio Carousel Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    // Dummy data for portfolio items
    final List portfolioItems = [
      PortfolioItem(
        imageUrl: 'assets/image1.jpg', // Make sure to add images to pubspec.yaml
        title: 'Project Alpha',
        description: 'A cutting-edge mobile application for productivity enthusiasts.',
      ),
      PortfolioItem(
        imageUrl: 'assets/image2.jpg',
        title: 'Website Redesign',
        description: 'Modern and responsive web design for an e-commerce platform.',
      ),
      PortfolioItem(
        imageUrl: 'assets/image3.jpg',
        title: 'UI/UX Case Study',
        description: 'Detailed analysis and design solutions for a fintech app.',
      ),
    ];

    return Scaffold(
      appBar: AppBar(
        title: const Text('My Portfolio'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              'Featured Works:',
              style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 20),
            PortfolioCarouselWidget(items: portfolioItems),
            // You can add more widgets here, like page indicators based on _currentPage
          ],
        ),
      ),
    );
  }
}

Remember to update your pubspec.yaml file to include your assets. Create an assets folder in your project root and place your images there.


flutter:
  uses-material-design: true
  assets:
    - assets/image1.jpg
    - assets/image2.jpg
    - assets/image3.jpg
    # Add all your image paths here

Explanation of Key Parts

Let's delve into the important aspects of the code:

  • PortfolioItem Data Model: A simple class to keep your data organized, making it easy to pass project details around.
  • _PortfolioCarouselWidgetState:
    • PageController: Initialized with viewportFraction: 1.0 to ensure each page takes up the full width of the PageView. A listener is added to update _currentPage when the user swipes, which can be useful for adding page indicators later.
    • PageView.builder: Efficiently builds only the visible pages. itemBuilder constructs each slide using the _buildPortfolioCard method.
  • _buildPortfolioCard(PortfolioItem item):
    • Padding and ClipRRect: Adds some horizontal spacing between carousel items and gives them rounded corners, enhancing visual appeal.
    • Stack: The core for layering. The Image.asset fills the background, ensuring the portfolio item's image is prominent.
    • Positioned.fill: This widget makes its child (the Container acting as the overlay) expand to fill the entire available space of the Stack.
    • Overlay Container with LinearGradient: Instead of a solid color overlay, we use a LinearGradient. It starts transparent at the top and transitions to a semi-transparent black at the bottom. This allows the top part of the image to be fully visible while providing a clear, readable background for the text at the bottom. The stops property controls where the color transition occurs.
    • Padding and Column for Text: Inside the overlay, Padding ensures the text isn't cramped. A Column with mainAxisAlignment: MainAxisAlignment.end pushes the title and description to the bottom of the overlay, creating a modern layout. Styling ensures readability with white text against the dark gradient.

Conclusion

You have now successfully created a professional and highly customizable portfolio carousel widget in Flutter. This widget is a fantastic way to showcase your projects with visual flair, making your application or website more engaging for users. You can further enhance this widget by adding dot indicators, autoplay functionality, or even dynamic animations for the overlay and text based on page transitions.

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