image

01 Jan 2026

9K

35K

Building a Step Progress Tracker Widget in Flutter

A step progress tracker is a common UI element used to visualize a user's progress through a multi-step process, such as a checkout flow, a registration form, or an onboarding sequence. It provides clear visual cues, enhancing user experience by indicating the current step, completed steps, and upcoming steps. This article will guide you through building a customizable step progress tracker widget in Flutter, covering its core components and implementation details.

Core Components of a Step Progress Tracker

Before diving into the code, let's break down the essential components of a typical step progress tracker:

  • Steps: Each individual stage in the process.
  • Current Step: The step the user is currently on.
  • Visual Indicators: Elements like circles, icons, or numbers to represent each step.
  • Connectors: Lines or bars that link the steps, showing the flow.
  • Labels (Optional): Text descriptions for each step.
  • States: Each step can be in one of three states: completed, active, or upcoming.

Designing the Step Data Structure

To manage the data for each step, we'll define a simple model. This allows us to easily pass step-specific information like titles, subtitles, and custom icons.


import 'package:flutter/material.dart';

class Step {
  final String title;
  final String? subtitle;
  final IconData? icon;

  Step({required this.title, this.subtitle, this.icon});
}

Creating the Individual Step Indicator Widget

Each step will be a distinct visual component, including an indicator (circle/icon) and optional text labels. We'll differentiate its appearance based on its state: completed, active, or upcoming. This widget will be private as it's an internal helper for the main tracker.


enum StepStatus {
  completed,
  active,
  upcoming,
}

class _StepIndicator extends StatelessWidget {
  final Step step;
  final StepStatus status;
  final Color activeColor;
  final Color completedColor;
  final Color upcomingColor;
  final TextStyle activeTextStyle;
  final TextStyle completedTextStyle;
  final TextStyle upcomingTextStyle;
  final double iconSize;
  final double circleRadius;

  const _StepIndicator({
    required this.step,
    required this.status,
    required this.activeColor,
    required this.completedColor,
    required this.upcomingColor,
    required this.activeTextStyle,
    required this.completedTextStyle,
    required this.upcomingTextStyle,
    this.iconSize = 20.0,
    this.circleRadius = 16.0,
  });

  @override
  Widget build(BuildContext context) {
    Color indicatorColor;
    TextStyle titleTextStyle;
    IconData defaultIcon;

    switch (status) {
      case StepStatus.completed:
        indicatorColor = completedColor;
        titleTextStyle = completedTextStyle;
        defaultIcon = Icons.check;
        break;
      case StepStatus.active:
        indicatorColor = activeColor;
        titleTextStyle = activeTextStyle;
        defaultIcon = Icons.circle; 
        break;
      case StepStatus.upcoming:
        indicatorColor = upcomingColor;
        titleTextStyle = upcomingTextStyle;
        defaultIcon = Icons.circle_outlined;
        break;
    }

    return Column(
      children: [
        Container(
          width: circleRadius * 2,
          height: circleRadius * 2,
          decoration: BoxDecoration(
            color: indicatorColor,
            shape: BoxShape.circle,
          ),
          child: Icon(
            step.icon ?? defaultIcon,
            color: Colors.white,
            size: iconSize,
          ),
        ),
        if (step.title.isNotEmpty)
          Padding(
            padding: const EdgeInsets.only(top: 8.0),
            child: Text(
              step.title,
              style: titleTextStyle,
              textAlign: TextAlign.center,
            ),
          ),
        if (step.subtitle != null && step.subtitle!.isNotEmpty)
          Text(
            step.subtitle!,
            style: titleTextStyle.copyWith(fontSize: 12), // Smaller subtitle
            textAlign: TextAlign.center,
          ),
      ],
    );
  }
}

Building the Step Progress Tracker Widget

This `StepProgressTracker` widget will orchestrate the layout of individual steps and their connectors. We'll use a `Row` to arrange the steps horizontally and `Expanded` widgets for the connector lines to ensure they fill the available space evenly. The `List.generate` method will create an alternating pattern of step indicators and connector lines.


class StepProgressTracker extends StatelessWidget {
  final List steps;
  final int currentStep;
  final Color activeColor;
  final Color completedColor;
  final Color upcomingColor;
  final Color lineColor;
  final double lineThickness;
  final double circleRadius;
  final double iconSize;
  final TextStyle activeTextStyle;
  final TextStyle completedTextStyle;
  final TextStyle upcomingTextStyle;

  const StepProgressTracker({
    Key? key,
    required this.steps,
    required this.currentStep,
    this.activeColor = Colors.blue,
    this.completedColor = Colors.green,
    this.upcomingColor = Colors.grey,
    this.lineColor = Colors.grey,
    this.lineThickness = 2.0,
    this.circleRadius = 16.0,
    this.iconSize = 20.0,
    this.activeTextStyle = const TextStyle(color: Colors.blue, fontWeight: FontWeight.bold),
    this.completedTextStyle = const TextStyle(color: Colors.green),
    this.upcomingTextStyle = const TextStyle(color: Colors.grey),
  }) : assert(currentStep >= 0 && currentStep < steps.length, 'currentStep must be a valid index within the steps list'),
       super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Row(
          children: List.generate(steps.length * 2 - 1, (index) {
            if (index.isEven) {
              // This is a step indicator
              final stepIndex = index ~/ 2;
              StepStatus status;
              if (stepIndex < currentStep) {
                status = StepStatus.completed;
              } else if (stepIndex == currentStep) {
                status = StepStatus.active;
              } else {
                status = StepStatus.upcoming;
              }
              return _StepIndicator(
                step: steps[stepIndex],
                status: status,
                activeColor: activeColor,
                completedColor: completedColor,
                upcomingColor: upcomingColor,
                activeTextStyle: activeTextStyle,
                completedTextStyle: completedTextStyle,
                upcomingTextStyle: upcomingTextStyle,
                iconSize: iconSize,
                circleRadius: circleRadius,
              );
            } else {
              // This is a connector line
              final previousStepIndex = (index - 1) ~/ 2;
              final isCompletedLine = previousStepIndex < currentStep;

              return Expanded(
                child: Container(
                  height: lineThickness,
                  color: isCompletedLine ? completedColor : lineColor,
                  margin: EdgeInsets.symmetric(horizontal: 4.0),
                ),
              );
            }
          }),
        ),
      ],
    );
  }
}

Example Usage

To integrate the `StepProgressTracker` into your Flutter application, you simply need to provide a list of `Step` objects and the `currentStep` index. The example below demonstrates how to use the widget within a basic Flutter app and how to dynamically update the current step.


import 'package:flutter/material.dart';

// Assuming Step, _StepIndicator, StepProgressTracker are defined
// in the same file or properly imported.

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

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

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

class _MyAppState extends State {
  int _currentStep = 0;

  final List _steps = [
    Step(title: 'Cart', subtitle: 'Items selected', icon: Icons.shopping_cart),
    Step(title: 'Address', subtitle: 'Shipping info', icon: Icons.location_on),
    Step(title: 'Payment', subtitle: 'Choose method', icon: Icons.payment),
    Step(title: 'Confirm', subtitle: 'Order review', icon: Icons.check_circle),
  ];

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Step Progress Tracker Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Step Progress Tracker'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: StepProgressTracker(
                  steps: _steps,
                  currentStep: _currentStep,
                  activeColor: Colors.deepPurple,
                  completedColor: Colors.teal,
                  upcomingColor: Colors.orange,
                  lineColor: Colors.grey.shade300,
                  lineThickness: 3.0,
                  iconSize: 24.0,
                  circleRadius: 20.0,
                  activeTextStyle: const TextStyle(color: Colors.deepPurple, fontWeight: FontWeight.bold, fontSize: 14),
                  completedTextStyle: const TextStyle(color: Colors.teal, fontSize: 13),
                  upcomingTextStyle: const TextStyle(color: Colors.orange.shade700, fontSize: 13),
                ),
              ),
              const SizedBox(height: 50),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: [
                  ElevatedButton(
                    onPressed: _currentStep > 0
                        ? () {
                            setState(() {
                              _currentStep--;
                            });
                          }
                        : null,
                    child: const Text('Previous'),
                  ),
                  ElevatedButton(
                    onPressed: _currentStep < _steps.length - 1
                        ? () {
                            setState(() {
                              _currentStep++;
                            });
                          }
                        : null,
                    child: const Text('Next'),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Conclusion

Building a custom step progress tracker in Flutter allows for a high degree of customization and provides a visually intuitive way to guide users through multi-step processes. By breaking down the problem into individual step indicators and connectors, and handling different states, we can create a reusable and robust widget. This implementation offers basic customization for colors, styles, and icons, making it adaptable to various application themes. Further enhancements could include animations for transitions between steps, support for vertical layouts, or more complex interaction handling like tapping on a step to navigate directly.

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