image

25 Jan 2026

9K

35K

Building Multi-Step Stepper Form Widgets in Flutter

Multi-step forms are a common UI pattern used to break down complex data entry processes into smaller, manageable sections. This approach improves user experience by reducing cognitive load, providing clear progress indicators, and making forms less intimidating. In Flutter, the Stepper widget is a powerful tool designed specifically for this purpose, allowing developers to create elegant multi-step forms with ease.

Understanding the Flutter Stepper Widget

The Stepper widget in Flutter provides a visual representation of a sequence of steps. Each step can contain its own content, often including form fields. Users can navigate between steps, and the widget can manage the current step's state and validate input before proceeding.

Key Properties of Stepper:

  • steps: A list of Step widgets, defining each stage of the process.
  • currentStep: An integer representing the index of the currently active step.
  • onStepContinue: A callback function triggered when the "Continue" button is pressed. This is where you typically implement validation and navigate to the next step.
  • onStepCancel: A callback function triggered when the "Cancel" (or "Back") button is pressed. Used for navigating to the previous step.
  • onStepTapped: A callback function triggered when a step's header is tapped, allowing direct navigation to a specific step.
  • type: Defines the orientation of the stepper (StepperType.vertical or StepperType.horizontal).

Key Properties of Step:

  • title: A Widget, usually a Text widget, displayed as the step's title.
  • content: A Widget that holds the main content of the step, such as form fields.
  • isActive: A boolean indicating if the step is currently active.
  • state: A StepState enum (indexed, editing, complete, disabled, error) to convey the step's status.

Basic Implementation of a Stepper Form

Let's start by building a simple multi-step form that collects basic user information. We will manage the current step using a state variable and handle navigation.

Step 1: Set up the Stepper Widget

We'll create a StatefulWidget to manage the state of our stepper, including the currentStep index and data collected from each step.


import 'package:flutter/material.dart';

class MultiStepStepperForm extends StatefulWidget {
  const MultiStepStepperForm({super.key});

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

class _MultiStepStepperFormState extends State {
  int _currentStep = 0;
  String? _name;
  String? _email;
  String? _address;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Multi-Step Stepper Form'),
      ),
      body: Stepper(
        type: StepperType.vertical,
        currentStep: _currentStep,
        onStepContinue: () {
          final isLastStep = _currentStep == getSteps().length - 1;
          if (isLastStep) {
            // Logic to submit the form
            print('Form Submitted!');
            print('Name: $_name');
            print('Email: $_email');
            print('Address: $_address');
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(content: Text('Form Submitted Successfully!')),
            );
          } else {
            setState(() {
              _currentStep += 1;
            });
          }
        },
        onStepCancel: () {
          if (_currentStep > 0) {
            setState(() {
              _currentStep -= 1;
            });
          }
        },
        onStepTapped: (step) {
          setState(() {
            _currentStep = step;
          });
        },
        steps: getSteps(),
      ),
    );
  }

  List getSteps() {
    return [
      Step(
        title: const Text('Account Info'),
        content: Column(
          children: [
            TextFormField(
              decoration: const InputDecoration(labelText: 'Name'),
              onChanged: (value) => setState(() => _name = value),
            ),
            TextFormField(
              decoration: const InputDecoration(labelText: 'Email'),
              onChanged: (value) => setState(() => _email = value),
            ),
          ],
        ),
        isActive: _currentStep >= 0,
        state: _currentStep > 0 ? StepState.complete : StepState.indexed,
      ),
      Step(
        title: const Text('Personal Address'),
        content: Column(
          children: [
            TextFormField(
              decoration: const InputDecoration(labelText: 'Address'),
              onChanged: (value) => setState(() => _address = value),
            ),
          ],
        ),
        isActive: _currentStep >= 1,
        state: _currentStep > 1 ? StepState.complete : StepState.indexed,
      ),
      Step(
        title: const Text('Confirmation'),
        content: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Name: ${_name ?? ''}'),
            Text('Email: ${_email ?? ''}'),
            Text('Address: ${_address ?? ''}'),
            const SizedBox(height: 16),
            const Text('Please review your information before submitting.'),
          ],
        ),
        isActive: _currentStep >= 2,
        state: _currentStep == 2 ? StepState.editing : StepState.indexed,
      ),
    ];
  }
}

In this example, we define three steps using the getSteps() method. Each step contains TextFormField widgets to collect user input. The onStepContinue, onStepCancel, and onStepTapped callbacks update the _currentStep state variable, which in turn rebuilds the Stepper to show the active step.

Adding Form Validation

For robust forms, validation is essential. Flutter's Form widget combined with GlobalKey allows us to validate individual steps before proceeding.

Step 2: Integrate Form and Validation

We will modify the getSteps() method to wrap each step's content in a Form widget and add validators to the TextFormField widgets. We'll also need a list of GlobalKey, one for each step.


import 'package:flutter/material.dart';

class MultiStepStepperFormWithValidation extends StatefulWidget {
  const MultiStepStepperFormWithValidation({super.key});

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

class _MultiStepStepperFormWithValidationState extends State {
  int _currentStep = 0;
  String? _name;
  String? _email;
  String? _address;

  // List of GlobalKeys for form validation, one for each step
  final List> _formKeys = [
    GlobalKey(), // Key for Step 1
    GlobalKey(), // Key for Step 2
    GlobalKey(), // Key for Step 3 (optional, as it's just confirmation)
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Multi-Step Stepper Form (with Validation)'),
      ),
      body: Stepper(
        type: StepperType.vertical,
        currentStep: _currentStep,
        onStepContinue: () {
          // Validate the current step's form
          if (_formKeys[_currentStep].currentState!.validate()) {
            _formKeys[_currentStep].currentState!.save(); // Save the form data

            final isLastStep = _currentStep == getSteps().length - 1;
            if (isLastStep) {
              // Logic to submit the form
              print('Form Submitted!');
              print('Name: $_name');
              print('Email: $_email');
              print('Address: $_address');
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('Form Submitted Successfully!')),
              );
            } else {
              setState(() {
                _currentStep += 1;
              });
            }
          }
        },
        onStepCancel: () {
          if (_currentStep > 0) {
            setState(() {
              _currentStep -= 1;
            });
          }
        },
        onStepTapped: (step) {
          // Allow direct navigation only if previous steps are valid (optional, depends on UX)
          // For simplicity, we'll allow tapping to any step for now, but
          // a production app might restrict this.
          setState(() {
            _currentStep = step;
          });
        },
        steps: getSteps(),
      ),
    );
  }

  List getSteps() {
    return [
      Step(
        title: const Text('Account Info'),
        content: Form(
          key: _formKeys[0], // Assign key for validation
          child: Column(
            children: [
              TextFormField(
                decoration: const InputDecoration(labelText: 'Name'),
                initialValue: _name,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter your name.';
                  }
                  return null;
                },
                onSaved: (value) => _name = value,
              ),
              TextFormField(
                decoration: const InputDecoration(labelText: 'Email'),
                initialValue: _email,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter your email.';
                  }
                  if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
                    return 'Please enter a valid email address.';
                  }
                  return null;
                },
                onSaved: (value) => _email = value,
              ),
            ],
          ),
        ),
        isActive: _currentStep >= 0,
        state: _currentStep > 0 ? StepState.complete : StepState.indexed,
      ),
      Step(
        title: const Text('Personal Address'),
        content: Form(
          key: _formKeys[1], // Assign key for validation
          child: Column(
            children: [
              TextFormField(
                decoration: const InputDecoration(labelText: 'Address'),
                initialValue: _address,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter your address.';
                  }
                  return null;
                },
                onSaved: (value) => _address = value,
              ),
            ],
          ),
        ),
        isActive: _currentStep >= 1,
        state: _currentStep > 1 ? StepState.complete : StepState.indexed,
      ),
      Step(
        title: const Text('Confirmation'),
        content: Form( // Wrap in Form even if no validation, for consistency
          key: _formKeys[2],
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('Name: ${_name ?? ''}'),
              Text('Email: ${_email ?? ''}'),
              Text('Address: ${_address ?? ''}'),
              const SizedBox(height: 16),
              const Text('Please review your information before submitting.'),
            ],
          ),
        ),
        isActive: _currentStep >= 2,
        state: _currentStep == 2 ? StepState.editing : StepState.indexed,
      ),
    ];
  }
}

In this enhanced version:

  • A List> _formKeys is used to uniquely identify each step's form.
  • Each step's content is wrapped in a Form widget, and its respective GlobalKey is assigned.
  • TextFormField widgets now include a validator function to check input validity and an onSaved callback to store the value once validated.
  • Inside onStepContinue, we call _formKeys[_currentStep].currentState!.validate(). The stepper will only proceed to the next step if the current step's form is valid. If valid, _formKeys[_currentStep].currentState!.save() is called to trigger the onSaved callbacks and update the state variables.
  • The state property of each Step is updated dynamically to show whether a step is complete, editing, or indexed, providing better visual feedback.

Conclusion

Building multi-step stepper forms in Flutter using the Stepper widget is an effective way to improve user experience for complex data entry. By combining Stepper with Flutter's Form and validation capabilities, developers can create interactive, validated forms that guide users through a logical flow. This pattern not only makes forms easier to complete but also ensures data integrity before submission. Further enhancements can include custom stepper controls, animations, and integration with more advanced state management solutions like Provider or BLoC for larger 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