image

11 Apr 2026

9K

35K

Building an Animated & Validated Stepper Form Widget in Flutter

Introduction

Stepper forms are a common UI pattern used to break down complex forms or multi-stage processes into smaller, manageable steps. This improves user experience by reducing cognitive load and providing a clear sense of progress. In Flutter, building a stepper form that includes both robust validation and smooth animated transitions can significantly enhance its usability and visual appeal.

In this article, we will walk through the process of creating a custom stepper form widget in Flutter. We'll cover:

  • Defining a data model for each step.
  • Structuring the main stepper widget.
  • Implementing per-step form validation.
  • Adding animated transitions between steps for a polished look.

Defining the Step Data Model

First, let's define a simple data structure to represent each step in our form. Each step will need a title, the actual form content widget, and a GlobalKey for its validation.


import 'package:flutter/material.dart';

class FormStep {
  final String title;
  final Widget content;
  final GlobalKey formKey;

  FormStep({
    required this.title,
    required this.content,
    required this.formKey,
  });
}

The Core Stepper Widget Structure

Our main stepper form will be a StatefulWidget to manage the current step index and other states. We'll initialize a list of FormStep objects and manage the _currentStepIndex.


import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; // For Widget

// Assume FormStep class is defined as above

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

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

class _StepperFormPageState extends State {
  int _currentStepIndex = 0;
  late List _steps;

  @override
  void initState() {
    super.initState();
    _steps = [
      FormStep(
        title: 'Personal Info',
        content: Column(
          children: [
            TextFormField(
              decoration: const InputDecoration(labelText: 'First Name'),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter your first name';
                }
                return null;
              },
            ),
            TextFormField(
              decoration: const InputDecoration(labelText: 'Last Name'),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter your last name';
                }
                return null;
              },
            ),
          ],
        ),
        formKey: GlobalKey(),
      ),
      FormStep(
        title: 'Contact Details',
        content: Column(
          children: [
            TextFormField(
              decoration: const InputDecoration(labelText: 'Email'),
              keyboardType: TextInputType.emailAddress,
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter your email';
                }
                if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
                  return 'Please enter a valid email';
                }
                return null;
              },
            ),
            TextFormField(
              decoration: const InputDecoration(labelText: 'Phone'),
              keyboardType: TextInputType.phone,
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter your phone number';
                }
                return null;
              },
            ),
          ],
        ),
        formKey: GlobalKey(),
      ),
      FormStep(
        title: 'Review & Submit',
        content: const Center(
          child: Text('Please review your information before submitting.'),
        ),
        formKey: GlobalKey(), // This step might not need validation, but key is good practice
      ),
    ];
  }

  void _nextStep() {
    if (_currentStepIndex < _steps.length - 1) {
      if (_steps[_currentStepIndex].formKey.currentState?.validate() ?? false) {
        setState(() {
          _currentStepIndex++;
        });
      }
    } else {
      // This is the last step, handle submission
      if (_steps[_currentStepIndex].formKey.currentState?.validate() ?? false) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Form Submitted!')),
        );
      }
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Animated Stepper Form'),
      ),
      body: Column(
        children: [
          // Step indicators (optional, but good for UX)
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: _steps.asMap().entries.map((entry) {
                int index = entry.key;
                FormStep step = entry.value;
                return GestureDetector(
                  onTap: () {
                    // Optional: allow jumping to steps if validation is handled
                    // For simplicity, we'll navigate using buttons
                  },
                  child: Column(
                    children: [
                      CircleAvatar(
                        backgroundColor: _currentStepIndex >= index
                            ? Theme.of(context).primaryColor
                            : Colors.grey,
                        child: Text(
                          '${index + 1}',
                          style: const TextStyle(color: Colors.white),
                        ),
                      ),
                      const SizedBox(height: 4),
                      Text(
                        step.title,
                        style: TextStyle(
                          fontWeight: _currentStepIndex == index
                              ? FontWeight.bold
                              : FontWeight.normal,
                          color: _currentStepIndex >= index
                              ? Colors.black87
                              : Colors.grey,
                        ),
                      ),
                    ],
                  ),
                );
              }).toList(),
            ),
          ),
          Expanded(
            child: AnimatedSwitcher(
              duration: const Duration(milliseconds: 300),
              transitionBuilder: (Widget child, Animation animation) {
                final offsetAnimation = Tween(
                  begin: const Offset(1.0, 0.0), // Starts from right
                  end: Offset.zero,
                ).animate(animation);
                return SlideTransition(
                  position: offsetAnimation,
                  child: FadeTransition(
                    opacity: animation,
                    child: child,
                  ),
                );
              },
              child: Container(
                key: ValueKey(_currentStepIndex), // Key is crucial for AnimatedSwitcher
                padding: const EdgeInsets.all(16.0),
                child: Form(
                  key: _steps[_currentStepIndex].formKey,
                  child: _steps[_currentStepIndex].content,
                ),
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                if (_currentStepIndex > 0)
                  ElevatedButton(
                    onPressed: _previousStep,
                    child: const Text('Previous'),
                  ),
                const Spacer(),
                ElevatedButton(
                  onPressed: _nextStep,
                  child: Text(_currentStepIndex == _steps.length - 1 ? 'Submit' : 'Next'),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Implementing Form Validation

Form validation is critical in a stepper form to ensure data integrity before proceeding to the next step. Each step's content is wrapped in a Form widget, which uses its associated GlobalKey to trigger validation.

In the _nextStep method, we call _steps[_currentStepIndex].formKey.currentState?.validate(). This method returns true if all form fields within that step's Form are valid, and false otherwise. If valid, the stepper can proceed; if not, validation messages will be displayed by the TextFormField widgets.


  // Inside _StepperFormPageState
  void _nextStep() {
    if (_currentStepIndex < _steps.length - 1) {
      // Validate the current step's form
      if (_steps[_currentStepIndex].formKey.currentState?.validate() ?? false) {
        setState(() {
          _currentStepIndex++;
        });
      }
    } else {
      // This is the last step, handle submission
      if (_steps[_currentStepIndex].formKey.currentState?.validate() ?? false) {
        // Here you would typically collect all data and send it
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Form Submitted!')),
        );
      }
    }
  }

Each TextFormField inside a step's content needs a validator function to define its validation rules.


// Example TextFormField with validator
TextFormField(
  decoration: const InputDecoration(labelText: 'First Name'),
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Please enter your first name';
    }
    return null;
  },
),

Enhancing with Animated Transitions

To make the step transitions smooth and engaging, we use Flutter's AnimatedSwitcher widget. AnimatedSwitcher automatically animates between two child widgets when its child property changes, provided the children have unique Keys.

We wrap the current step's content in an AnimatedSwitcher. The key of the content is set to ValueKey(_currentStepIndex), ensuring that AnimatedSwitcher detects a change and triggers the animation whenever _currentStepIndex updates.


// Inside _StepperFormPageState's build method
Expanded(
  child: AnimatedSwitcher(
    duration: const Duration(milliseconds: 300),
    transitionBuilder: (Widget child, Animation animation) {
      // Custom slide and fade transition
      final offsetAnimation = Tween(
        begin: const Offset(1.0, 0.0), // Starts from right
        end: Offset.zero,
      ).animate(animation);
      return SlideTransition(
        position: offsetAnimation,
        child: FadeTransition(
          opacity: animation,
          child: child,
        ),
      );
    },
    child: Container(
      // Key is crucial for AnimatedSwitcher to detect changes
      key: ValueKey(_currentStepIndex),
      padding: const EdgeInsets.all(16.0),
      child: Form(
        key: _steps[_currentStepIndex].formKey,
        child: _steps[_currentStepIndex].content,
      ),
    ),
  ),
),

The transitionBuilder allows us to define a custom animation. Here, we've combined a SlideTransition (moving from right to left) with a FadeTransition for a pleasant visual effect. You can customize this to any animation type you prefer.

Putting It All Together

Here's how you can integrate the StepperFormPage into a basic Flutter application:


import 'package:flutter/material.dart';
// Assuming form_step.dart and stepper_form_page.dart files exist
// import 'form_step.dart';
// import 'stepper_form_page.dart';

// Or, if all code is in one file:
// Define FormStep class here
// Define StepperFormPage and _StepperFormPageState here

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Stepper Form Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const StepperFormPage(),
    );
  }
}

Conclusion

By combining a well-defined step data model, per-step form validation, and smooth animated transitions, we've built a robust and user-friendly stepper form widget in Flutter. This approach enhances the user experience by breaking down complex input processes into manageable segments, guiding users effectively through their journey.

You can further extend this implementation by adding a global progress indicator, allowing users to jump to previous steps (with appropriate validation handling), or integrating more complex 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