image

17 Mar 2026

9K

35K

Creating a Recipe Stepper Widget with Image Carousel in Flutter

Building intuitive and engaging user interfaces is crucial for any successful mobile application. When it comes to displaying multi-step content, such as recipes, tutorials, or onboarding flows, a stepper widget provides a structured and easy-to-follow experience. Integrating an image carousel at the top can further enhance this by visually showcasing the end product or key ingredients as the user progresses.

In this article, we'll walk through the process of creating a dynamic recipe stepper widget in Flutter, complete with an image carousel at the top, allowing users to navigate through recipe steps with ease.

Prerequisites

  • Basic understanding of Flutter and Dart.
  • Flutter SDK installed and configured.
  • A code editor like VS Code or Android Studio.

Project Setup

First, let's create a new Flutter project. Open your terminal or command prompt and run:


flutter create recipe_stepper_app
cd recipe_stepper_app

Now, open the project in your preferred IDE.

Designing the Data Model

For our recipe, we'll need a list of steps (instructions) and a list of images to display in the carousel. We'll keep it simple by using String lists for both.

Implementing the Image Carousel

We'll use Flutter's built-in PageView widget for the image carousel. PageView allows users to swipe horizontally between pages, which is perfect for displaying a gallery of images. We'll also add simple dot indicators to show the current page.

Here's how we can set up the image carousel:


import 'package:flutter/material.dart';

class RecipeStepperPage extends StatefulWidget {
  @override
  _RecipeStepperPageState createState() => _RecipeStepperPageState();
}

class _RecipeStepperPageState extends State {
  final PageController _pageController = PageController();
  int _currentImagePage = 0;

  final List _recipeImages = [
    'https://via.placeholder.com/600x400/FF5733/FFFFFF?text=Delicious+Meal+1',
    'https://via.placeholder.com/600x400/33FF57/FFFFFF?text=Fresh+Ingredients',
    'https://via.placeholder.com/600x400/3357FF/FFFFFF?text=Ready+to+Cook',
    'https://via.placeholder.com/600x400/FFFF33/000000?text=Serving+Suggestion',
  ];

  // ... (rest of the state and build method will come here)
}

Inside the build method, we'll integrate the PageView and the page indicators:


// ... inside _RecipeStepperPageState's build method

Scaffold(
  appBar: AppBar(
    title: Text('Recipe Stepper'),
    backgroundColor: Colors.deepOrange,
  ),
  body: Column(
    children: [
      // Image Carousel
      Container(
        height: 250,
        child: PageView.builder(
          controller: _pageController,
          itemCount: _recipeImages.length,
          onPageChanged: (index) {
            setState(() {
              _currentImagePage = index;
            });
          },
          itemBuilder: (context, index) {
            return Image.network(
              _recipeImages[index],
              fit: BoxFit.cover,
            );
          },
        ),
      ),
      // Image Carousel Indicators
      Padding(
        padding: const EdgeInsets.all(8.0),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: List.generate(_recipeImages.length, (index) {
            return AnimatedContainer(
              duration: Duration(milliseconds: 150),
              margin: EdgeInsets.symmetric(horizontal: 4.0),
              height: 8.0,
              width: _currentImagePage == index ? 24.0 : 8.0,
              decoration: BoxDecoration(
                color: _currentImagePage == index
                    ? Colors.deepOrange
                    : Colors.grey.withOpacity(0.5),
                borderRadius: BorderRadius.circular(4.0),
              ),
            );
          }),
        ),
      ),
      // ... (Stepper content will go here)
    ],
  ),
);

Implementing the Recipe Stepper Core

Next, we'll define the recipe steps and implement the stepper logic. This involves managing the current step index and providing "Next" and "Previous" buttons to navigate through the steps.


// ... inside _RecipeStepperPageState

  int _currentStep = 0;

  final List _recipeSteps = [
    'Step 1: Gather all your fresh ingredients. Make sure everything is prepped and ready to go.',
    'Step 2: Preheat your oven to 200°C (392°F). Prepare your baking dish as instructed.',
    'Step 3: Combine dry ingredients in one bowl and wet ingredients in another. Mix them separately.',
    'Step 4: Gently fold the wet ingredients into the dry ingredients until just combined. Do not overmix!',
    'Step 5: Pour the mixture into the prepared baking dish and bake for 30-40 minutes, or until golden brown.',
    'Step 6: Let it cool for 10 minutes before serving. Enjoy your delicious homemade meal!',
  ];

  void _nextStep() {
    setState(() {
      if (_currentStep < _recipeSteps.length - 1) {
        _currentStep++;
      } else {
        // Optionally, handle "finish" or "restart" here
        print('Recipe Finished!');
      }
    });
  }

  void _previousStep() {
    setState(() {
      if (_currentStep > 0) {
        _currentStep--;
      }
    });
  }

  // ... (rest of the build method after carousel)

Now, let's add the UI for displaying the current step's instruction and the navigation buttons below the carousel.


// ... inside _RecipeStepperPageState's build method, after the image carousel indicators

      Expanded(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                'Step ${_currentStep + 1} of ${_recipeSteps.length}',
                style: TextStyle(
                  fontSize: 18,
                  fontWeight: FontWeight.bold,
                  color: Colors.deepOrange,
                ),
              ),
              SizedBox(height: 10),
              Text(
                _recipeSteps[_currentStep],
                style: TextStyle(fontSize: 16),
              ),
              Spacer(), // Pushes buttons to the bottom
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  ElevatedButton(
                    onPressed: _currentStep > 0 ? _previousStep : null,
                    style: ElevatedButton.styleFrom(
                      primary: Colors.grey, // Disabled color
                      onSurface: Colors.deepOrange, // Enabled color
                    ),
                    child: Text('Previous'),
                  ),
                  ElevatedButton(
                    onPressed: _currentStep < _recipeSteps.length - 1
                        ? _nextStep
                        : null,
                    style: ElevatedButton.styleFrom(
                      primary: Colors.deepOrange,
                    ),
                    child: Text(
                      _currentStep < _recipeSteps.length - 1 ? 'Next' : 'Finish',
                    ),
                  ),
                ],
              ),
              SizedBox(height: 16), // Bottom padding for buttons
            ],
          ),
        ),
      ),
    ],
  ),
);

Putting It All Together

Finally, let's integrate everything into a complete Flutter application. Replace the contents of your main.dart file with the following:


import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Recipe Stepper App',
      theme: ThemeData(
        primarySwatch: Colors.deepOrange,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: RecipeStepperPage(),
    );
  }
}

class RecipeStepperPage extends StatefulWidget {
  @override
  _RecipeStepperPageState createState() => _RecipeStepperPageState();
}

class _RecipeStepperPageState extends State {
  final PageController _pageController = PageController();
  int _currentImagePage = 0;
  int _currentStep = 0;

  final List _recipeImages = [
    'https://via.placeholder.com/600x400/FF5733/FFFFFF?text=Delicious+Meal+1',
    'https://via.placeholder.com/600x400/33FF57/FFFFFF?text=Fresh+Ingredients',
    'https://via.placeholder.com/600x400/3357FF/FFFFFF?text=Ready+to+Cook',
    'https://via.placeholder.com/600x400/FFFF33/000000?text=Serving+Suggestion',
  ];

  final List _recipeSteps = [
    'Step 1: Gather all your fresh ingredients. Make sure everything is prepped and ready to go.',
    'Step 2: Preheat your oven to 200°C (392°F). Prepare your baking dish as instructed.',
    'Step 3: Combine dry ingredients in one bowl and wet ingredients in another. Mix them separately.',
    'Step 4: Gently fold the wet ingredients into the dry ingredients until just combined. Do not overmix!',
    'Step 5: Pour the mixture into the prepared baking dish and bake for 30-40 minutes, or until golden brown.',
    'Step 6: Let it cool for 10 minutes before serving. Enjoy your delicious homemade meal!',
  ];

  void _nextStep() {
    setState(() {
      if (_currentStep < _recipeSteps.length - 1) {
        _currentStep++;
      } else {
        // Optionally, handle "finish" or "restart" here
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Recipe Completed!')),
        );
      }
    });
  }

  void _previousStep() {
    setState(() {
      if (_currentStep > 0) {
        _currentStep--;
      }
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Recipe Stepper'),
        backgroundColor: Colors.deepOrange,
        elevation: 0,
      ),
      body: Column(
        children: [
          // Image Carousel
          Container(
            height: 250,
            child: PageView.builder(
              controller: _pageController,
              itemCount: _recipeImages.length,
              onPageChanged: (index) {
                setState(() {
                  _currentImagePage = index;
                });
              },
              itemBuilder: (context, index) {
                return Image.network(
                  _recipeImages[index],
                  fit: BoxFit.cover,
                  errorBuilder: (context, error, stackTrace) {
                    return Center(child: Icon(Icons.broken_image, size: 50, color: Colors.grey));
                  },
                );
              },
            ),
          ),
          // Image Carousel Indicators
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: List.generate(_recipeImages.length, (index) {
                return AnimatedContainer(
                  duration: Duration(milliseconds: 150),
                  margin: EdgeInsets.symmetric(horizontal: 4.0),
                  height: 8.0,
                  width: _currentImagePage == index ? 24.0 : 8.0,
                  decoration: BoxDecoration(
                    color: _currentImagePage == index
                        ? Colors.deepOrange
                        : Colors.grey.withOpacity(0.5),
                    borderRadius: BorderRadius.circular(4.0),
                  ),
                );
              }),
            ),
          ),
          // Stepper Content
          Expanded(
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Step ${_currentStep + 1} of ${_recipeSteps.length}',
                    style: TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                      color: Colors.deepOrange,
                    ),
                  ),
                  SizedBox(height: 10),
                  Expanded( // Use Expanded for the text to allow scrolling if content is long
                    child: SingleChildScrollView(
                      child: Text(
                        _recipeSteps[_currentStep],
                        style: TextStyle(fontSize: 16),
                      ),
                    ),
                  ),
                  SizedBox(height: 20), // Spacer before buttons
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      ElevatedButton(
                        onPressed: _currentStep > 0 ? _previousStep : null,
                        style: ElevatedButton.styleFrom(
                          primary: Colors.blueGrey, // Use a distinct color for previous
                          onSurface: Colors.blueGrey.shade200, // Disabled color
                          padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
                          shape: RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(8),
                          ),
                        ),
                        child: Text(
                          'Previous',
                          style: TextStyle(color: Colors.white),
                        ),
                      ),
                      ElevatedButton(
                        onPressed: _currentStep < _recipeSteps.length - 1
                            ? _nextStep
                            : (_currentStep == _recipeSteps.length - 1 ? _nextStep : null),
                        style: ElevatedButton.styleFrom(
                          primary: Colors.deepOrange,
                          padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
                          shape: RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(8),
                          ),
                        ),
                        child: Text(
                          _currentStep < _recipeSteps.length - 1 ? 'Next' : 'Finish',
                          style: TextStyle(color: Colors.white),
                        ),
                      ),
                    ],
                  ),
                  SizedBox(height: 16), // Bottom padding
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Conclusion and Further Enhancements

You've successfully built a recipe stepper widget with an integrated image carousel in Flutter! This setup provides a clean and interactive way for users to follow multi-step instructions while visually engaging with related content.

Here are some ideas for further enhancements:

  • Complex Data Model: Enhance the _recipeSteps to be a list of custom objects, each containing not just text but also step-specific images, timers, or ingredient lists.
  • Animations: Add more sophisticated animations for step transitions or image changes.
  • Accessibility: Implement proper semantics for screen readers.
  • Error Handling: Implement more robust error handling for network images.
  • Persist Progress: Use local storage (e.g., shared_preferences or a database) to save the user's progress through a recipe.
  • Third-party Packages: Explore packages like carousel_slider for more advanced carousel features or smooth_page_indicator for more indicator styles.
  • Gestures: Allow swiping left/right on the recipe text area to navigate steps as well.

This widget forms a solid foundation for any app requiring a guided, multi-step user experience. Happy coding!

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