image

21 Dec 2025

9K

35K

Creating a Step Progress Indicator Widget in Flutter

In modern applications, guiding users through multi-step processes is crucial for a smooth and intuitive user experience. A Step Progress Indicator is a common UI component used to visualize the user's current position within a sequence of tasks, such as onboarding flows, multi-page forms, or e-commerce checkout processes.

This article will guide you through creating a customizable Step Progress Indicator widget in Flutter, covering its core components, implementation strategy, and a complete code example.

Understanding the Core Components

A typical step progress indicator consists of several key elements:

  • Steps: Individual units representing each stage of the process. Each step usually has a visual identifier (like a number or icon) and a descriptive label.
  • Connectors (Dividers): Lines or other visual elements that link one step to the next, indicating the flow.
  • States: Each step can be in one of three states, requiring distinct visual styles:
    • Active (Current): The step the user is currently on.
    • Completed: Steps that the user has successfully finished.
    • Inactive (Upcoming): Steps that are yet to be reached.

Flutter Implementation Strategy

We'll build our Step Progress Indicator as a StatefulWidget to manage the current step. The layout will primarily use a Row widget to arrange the steps and their connecting dividers horizontally. Each individual step will be a Column containing an icon/number and its text label.

Key considerations for our implementation:

  • **Flexibility:** The widget should accept a list of step titles and the current step index as parameters.
  • **Customization:** Allow for custom colors for active, inactive, and completed states, as well as adjustable sizes.
  • **Clear Visuals:** Distinct styling for each step state to provide immediate feedback to the user.

Step-by-Step Code Walkthrough

Let's break down the creation of our StepProgressIndicator widget.

1. Define the StepProgressIndicator Widget

This widget will be a StatefulWidget to maintain its internal state, primarily responsible for rendering based on the provided steps and currentStep. It also defines customizable properties like colors and sizes.


import 'package:flutter/material.dart';

class StepProgressIndicator extends StatefulWidget {
  final List<String> steps;
  final int currentStep;
  final Color activeColor;
  final Color inactiveColor;
  final Color completedColor;
  final double stepRadius; // Radius of the circular step indicator
  final double lineHeight; // Height of the connecting line

  const StepProgressIndicator({
    Key? key,
    required this.steps,
    required this.currentStep,
    this.activeColor = Colors.blue,
    this.inactiveColor = Colors.grey,
    this.completedColor = Colors.green,
    this.stepRadius = 18.0,
    this.lineHeight = 4.0,
  }) : assert(currentStep >= 0 && currentStep <= steps.length),
       super(key: key);

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

2. Implement the State Class

The _StepProgressIndicatorState will contain the core logic for building the individual steps and dividers.


class _StepProgressIndicatorState extends State {
  // Method to build an individual step (circle + text label)
  Widget _buildStep(int index, String stepTitle) {
    bool isCompleted = index < widget.currentStep;
    bool isActive = index == widget.currentStep;
    bool isInactive = index > widget.currentStep;

    Color stepColor;
    Widget stepIcon;
    TextStyle stepTextStyle;

    if (isCompleted) {
      stepColor = widget.completedColor;
      stepIcon = Icon(Icons.check, color: Colors.white, size: widget.stepRadius * 1.2);
      stepTextStyle = TextStyle(color: widget.completedColor, fontWeight: FontWeight.bold);
    } else if (isActive) {
      stepColor = widget.activeColor;
      stepIcon = Text(
        '${index + 1}',
        style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: widget.stepRadius * 0.9),
      );
      stepTextStyle = TextStyle(color: widget.activeColor, fontWeight: FontWeight.bold);
    } else {
      stepColor = widget.inactiveColor;
      stepIcon = Text(
        '${index + 1}',
        style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: widget.stepRadius * 0.9),
      );
      stepTextStyle = TextStyle(color: widget.inactiveColor);
    }

    return Column(
      children: [
        Container(
          width: widget.stepRadius * 2,
          height: widget.stepRadius * 2,
          decoration: BoxDecoration(
            color: stepColor,
            shape: BoxShape.circle,
            border: Border.all(
              color: isActive ? widget.activeColor.withOpacity(0.5) : Colors.transparent, // Highlight active step
              width: isActive ? 2.0 : 0.0,
            ),
          ),
          child: Center(child: stepIcon),
        ),
        const SizedBox(height: 8.0),
        SizedBox(
          width: 80, // Fixed width for labels to prevent overflow
          child: Text(
            stepTitle,
            textAlign: TextAlign.center,
            style: stepTextStyle,
            overflow: TextOverflow.ellipsis,
          ),
        ),
      ],
    );
  }

  // Method to build the connecting line between steps
  Widget _buildDivider(int index) {
    bool isCompleted = index < widget.currentStep;
    Color lineColor = isCompleted ? widget.completedColor : widget.inactiveColor;

    return Expanded(
      child: Container(
        height: widget.lineHeight,
        color: lineColor,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        crossAxisAlignment: CrossAxisAlignment.start, // Align step labels at the top
        children: List.generate(widget.steps.length * 2 - 1, (index) {
          if (index.isEven) {
            // This is a step indicator
            final stepIndex = index ~/ 2;
            return _buildStep(stepIndex, widget.steps[stepIndex]);
          } else {
            // This is a divider line
            final dividerIndex = (index - 1) ~/ 2;
            return _buildDivider(dividerIndex);
          }
        }),
      ),
    );
  }
}

In the _buildStep method, we determine the step's state (completed, active, or inactive) and apply appropriate colors, icons (a checkmark for completed, a number for active/inactive), and text styles. We use a Column to stack the circular step indicator and its text label.

The _buildDivider method simply returns an Expanded Container, which acts as a line. Its color changes based on whether the preceding step is completed.

The main build method of _StepProgressIndicatorState generates a list of widgets alternating between steps and dividers, all laid out within a Row. List.generate is used to dynamically create these widgets based on the number of steps.

Complete Example Usage

To demonstrate how to use the StepProgressIndicator, let's create a simple Flutter application that allows you to advance through steps.


import 'package:flutter/material.dart';
// Assuming your StepProgressIndicator widget is in a file named
// 'step_progress_indicator.dart' in the same 'lib' folder or imported appropriately.
// import 'step_progress_indicator.dart'; // Uncomment if in a separate file

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Step Progress Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State {
  int _currentStep = 0;
  final List<String> _steps = [
    'Account',
    'Address',
    'Payment',
    'Confirm',
    'Done',
  ];

  void _nextStep() {
    setState(() {
      if (_currentStep < _steps.length - 1) {
        _currentStep++;
      } else {
        _currentStep = 0; // Reset for demo purposes
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Step Progress Indicator'),
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(20.0),
            child: StepProgressIndicator(
              steps: _steps,
              currentStep: _currentStep,
              activeColor: Colors.deepPurple,
              completedColor: Colors.teal,
              inactiveColor: Colors.blueGrey.shade200,
              stepRadius: 20.0,
              lineHeight: 5.0,
            ),
          ),
          const Expanded(
            child: Center(
              child: Text(
                'Navigate through the steps using the button below.',
                textAlign: TextAlign.center,
                style: TextStyle(fontSize: 16),
              ),
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _nextStep,
        child: const Icon(Icons.navigate_next),
      ),
    );
  }
}

Customization and Enhancements

The provided widget serves as a solid foundation. Here are some ways you can further customize and enhance it:

  • Theming: Introduce a dedicated theme or inherited widget to manage default colors, fonts, and sizes across your application.
  • Animations: Add explicit animations (e.g., using AnimatedContainer or TweenAnimationBuilder) when a step becomes active or completed for a more dynamic feel.
  • Vertical Layout: Adapt the widget to support a vertical orientation using a Column instead of a Row, and adjust divider logic accordingly.
  • Tappable Steps: Wrap the step indicators with GestureDetector or InkWell to allow users to tap on a step and potentially navigate directly to it (e.g., in a multi-page form where skipping steps is allowed).
  • Custom Icons: Allow passing custom widgets or IconData for step indicators instead of just numbers or checkmarks.
  • Dynamic Step Count: While our example uses a fixed list, ensure your application logic can handle dynamically changing step lists if needed.

Conclusion

A Step Progress Indicator is a powerful UI element that significantly improves user experience by providing clear visual feedback in multi-step processes. By following this guide, you can create a flexible and customizable widget in Flutter that fits various application needs. With further enhancements, it can become an even more interactive and visually appealing component in your Flutter toolkit.

Related Articles

Dec 21, 2025

Creating a Step Progress Indicator Widget

Creating a Step Progress Indicator Widget in Flutter In modern applications, guiding users through multi-step processes is crucial for a smooth and intuitive u

Dec 20, 2025

Flutter & Local Notifications: Building a Reminder App

Flutter & Local Notifications: Building a Reminder App Flutter, Google's UI toolkit for building natively compiled applications from a single codebase, offers

Dec 20, 2025

Building a Filter Search Widget in Flutter

Building a Filter Search Widget in Flutter Introduction In modern applications, users frequently interact with large datasets. To enhance user experience and e