image

16 Jan 2026

9K

35K

Building an Animated Step Progress Indicator Widget in Flutter

In modern user interfaces, providing clear feedback on progress through multi-step forms or workflows significantly enhances the user experience. A step progress indicator is a visual component that guides users by showing where they are in a sequence of steps, which steps are completed, and what's upcoming. Adding subtle animations to these indicators can further improve engagement and make the transitions feel more fluid and intuitive. This article will guide you through building a customizable animated step progress indicator widget in Flutter.

Understanding the Core Components

Before diving into the implementation, let's break down the essential components of our step progress indicator:

  • Steps Data Model: A way to represent each step, including its title, status (completed, current, upcoming), and potentially an icon.
  • Individual Step Item: A widget responsible for rendering a single step, displaying its icon/number, title, and reflecting its current status visually.
  • Connector Line: A line segment connecting two adjacent steps, whose appearance might change based on the progress.
  • Overall Layout: A container that arranges multiple step items and their connectors horizontally.
  • Animation: Smooth visual transitions for status changes, such as color changes for step circles or width/color changes for connector lines.

Step-by-Step Implementation

1. Defining the Step Data Model

First, let's create a simple class to encapsulate the data for each step. We'll include a title and a status enum.


enum StepStatus {
  completed,
  current,
  upcoming,
}

class StepData {
  final String title;
  final StepStatus status;
  final IconData? icon;

  StepData({required this.title, this.status = StepStatus.upcoming, this.icon});
}

2. Creating the Individual Step Item Widget

This widget will represent a single step in our progress indicator. It will display a circle (which can contain an icon or number) and the step's title. We'll use AnimatedContainer for smooth color transitions based on the step's status.


import 'package:flutter/material.dart';

// (Assuming StepData and StepStatus are defined as above)

class StepItem extends StatelessWidget {
  final StepData step;
  final int stepIndex;

  const StepItem({
    Key? key,
    required this.step,
    required this.stepIndex,
  }) : super(key: key);

  Color _getCircleColor(StepStatus status) {
    switch (status) {
      case StepStatus.completed:
        return Colors.green;
      case StepStatus.current:
        return Colors.blue;
      case StepStatus.upcoming:
        return Colors.grey.shade400;
    }
  }

  Color _getTextColor(StepStatus status) {
    switch (status) {
      case StepStatus.completed:
      case StepStatus.current:
        return Colors.black87;
      case StepStatus.upcoming:
        return Colors.grey.shade600;
    }
  }

  Widget _getCircleContent(StepStatus status, int index, IconData? icon) {
    switch (status) {
      case StepStatus.completed:
        return Icon(Icons.check, color: Colors.white, size: 16);
      case StepStatus.current:
        return Text('${index + 1}', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold));
      case StepStatus.upcoming:
        return Text('${index + 1}', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        AnimatedContainer(
          duration: Duration(milliseconds: 300),
          curve: Curves.easeInOut,
          width: 30,
          height: 30,
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            color: _getCircleColor(step.status),
          ),
          alignment: Alignment.center,
          child: _getCircleContent(step.status, stepIndex, step.icon),
        ),
        SizedBox(height: 8),
        Container(
          width: 80, // Fixed width for text to avoid overflow
          child: Text(
            step.title,
            textAlign: TextAlign.center,
            style: TextStyle(
              fontSize: 12,
              color: _getTextColor(step.status),
              fontWeight: step.status == StepStatus.current ? FontWeight.bold : FontWeight.normal,
            ),
            maxLines: 2,
            overflow: TextOverflow.ellipsis,
          ),
        ),
      ],
    );
  }
}

3. Assembling the Step Progress Indicator Widget

Now, let's create the main widget that arranges multiple StepItem widgets and the connector lines between them. We'll use a Row to lay them out horizontally and incorporate animated connector lines.


import 'package:flutter/material.dart';

// (Assuming StepData, StepStatus, and StepItem are defined as above)

class AnimatedStepProgressIndicator extends StatelessWidget {
  final List<StepData> steps;
  final double connectorThickness;
  final double connectorLength;

  const AnimatedStepProgressIndicator({
    Key? key,
    required this.steps,
    this.connectorThickness = 2.0,
    this.connectorLength = 40.0,
  }) : super(key: key);

  Color _getConnectorColor(StepStatus status) {
    switch (status) {
      case StepStatus.completed:
        return Colors.green;
      case StepStatus.current:
      case StepStatus.upcoming:
        return Colors.grey.shade400;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: List.generate(
        steps.length * 2 - 1,
        (index) {
          if (index.isOdd) {
            // Connector line
            final int prevStepIndex = (index - 1) ~/ 2;
            final StepStatus connectorStatus = steps[prevStepIndex].status == StepStatus.completed
                ? StepStatus.completed
                : StepStatus.upcoming;

            return AnimatedContainer(
              duration: Duration(milliseconds: 300),
              curve: Curves.easeInOut,
              height: connectorThickness,
              width: connectorLength,
              color: _getConnectorColor(connectorStatus),
              margin: EdgeInsets.symmetric(horizontal: 4), // Small margin to not touch circles
            );
          } else {
            // Step item
            final int stepIndex = index ~/ 2;
            return StepItem(
              step: steps[stepIndex],
              stepIndex: stepIndex,
            );
          }
        },
      ),
    );
  }
}

4. Integrating Animation

We've already used AnimatedContainer within both StepItem (for circle color) and AnimatedStepProgressIndicator (for connector line color). This widget automatically animates changes to its properties over a specified duration and curve, simplifying the animation process significantly. The key is to ensure that the properties being animated (like `color` or `width`) are changed in the parent widget, which triggers the `AnimatedContainer` to animate the transition.

5. Full Example Usage

To see our animated step progress indicator in action, let's create a simple Flutter application.


import 'package:flutter/material.dart';

// (Place StepData, StepStatus, StepItem, and AnimatedStepProgressIndicator classes here)

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  int _currentStepIndex = 0;

  List<StepData> _getSteps() {
    return [
      StepData(title: 'Cart', icon: Icons.shopping_cart),
      StepData(title: 'Address', icon: Icons.location_on),
      StepData(title: 'Payment', icon: Icons.payment),
      StepData(title: 'Confirm', icon: Icons.check_circle),
    ].asMap().entries.map((entry) {
      int index = entry.key;
      StepData step = entry.value;

      StepStatus status;
      if (index < _currentStepIndex) {
        status = StepStatus.completed;
      } else if (index == _currentStepIndex) {
        status = StepStatus.current;
      } else {
        status = StepStatus.upcoming;
      }
      return StepData(title: step.title, status: status, icon: step.icon);
    }).toList();
  }

  void _nextStep() {
    setState(() {
      if (_currentStepIndex < _getSteps().length - 1) {
        _currentStepIndex++;
      } else {
        // Optionally reset or show completion message
        _currentStepIndex = 0; // Reset for demonstration
      }
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Animated Step Progress',
      theme: ThemeData(
        primarySwatch: Colors.indigo,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Animated Step Progress Indicator'),
        ),
        body: Padding(
          padding: const EdgeInsets.all(20.0),
          child: Column(
            children: [
              AnimatedStepProgressIndicator(
                steps: _getSteps(),
                connectorLength: 60, // Adjust connector length as needed
              ),
              Spacer(),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  ElevatedButton(
                    onPressed: _currentStepIndex == 0 ? null : _previousStep,
                    child: Text('Previous'),
                  ),
                  ElevatedButton(
                    onPressed: _nextStep,
                    child: Text(_currentStepIndex == _getSteps().length - 1 ? 'Reset' : 'Next'),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

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

Customization and Enhancements

This basic setup provides a solid foundation. Here are ways you can further customize and enhance your widget:

  • Custom Icons: Modify _getCircleContent in StepItem to display custom icons from StepData instead of numbers or checkmarks.
  • Theming: Pass in custom colors, text styles, and sizes as parameters to AnimatedStepProgressIndicator to make it more flexible.
  • Tappable Steps: Wrap StepItem with a GestureDetector to allow users to jump to a specific step.
  • Orientation: Extend the widget to support vertical progress indicators.
  • More Complex Animations: For more intricate animations (e.g., a "bouncing" effect when a step becomes current), consider using TweenAnimationBuilder or explicit AnimationControllers.

Conclusion

Building an animated step progress indicator in Flutter provides a clear and engaging way to guide users through multi-step processes. By leveraging Flutter's declarative UI and built-in animation widgets like AnimatedContainer, we can create sophisticated and visually appealing components with relative ease. This widget is highly customizable and can be adapted to various design requirements, significantly improving the overall user experience of your Flutter applications.

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