Auth & Onboarding

Forgot Password Screen

A complete forgot-password screen with a validated email field, a send-reset-link flow with loading state, and a built-in success state that confirms the email was sent. It swaps cleanly between the form and the confirmation view inside one widget, so it plugs straight into Firebase Auth's sendPasswordResetEmail or any custom reset endpoint.

authemailform
9:41

Forgot password?

Enter your email and we'll send you a reset link.

you@example.com

Back to login

WHAT'S INCLUDED

  • Email TextFormField with required and regex email validation
  • Send reset link button with a CircularProgressIndicator loading state
  • Built-in success view confirming the reset email was sent to the entered address
  • Single widget that toggles between form and success with a _sent bool
  • Back to login actions on both the form and the success view
  • Keyboard done action wired to submit the form

USE CASES

  • Password reset entry point linked from the login screen
  • Account recovery flow for email-based authentication
  • Pair with Firebase Auth sendPasswordResetEmail for a working reset in minutes
  • Any "we will email you a link" confirmation pattern

PRO TIP

Show the same success state whether or not the email actually exists in your system. Revealing 'no account found' lets attackers enumerate which emails are registered; a neutral 'if an account exists, we sent a link' message is the standard secure pattern — and it is exactly what this screen's single success view makes easy.

Complete Dart Code

Copy this entire snippet into your Flutter project. No external packages required.

dart
import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        backgroundColor: Colors.white,
        body: Center(
          child: ForgotPasswordScreen(),
        ),
      ),
    );
  }
}

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

  @override
  State<ForgotPasswordScreen> createState() => _ForgotPasswordScreenState();
}

class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  bool _isLoading = false;
  bool _sent = false;

  @override
  void dispose() {
    _emailController.dispose();
    super.dispose();
  }

  Future<void> _onSend() async {
    if (!_formKey.currentState!.validate()) return;
    setState(() => _isLoading = true);
    await Future.delayed(const Duration(seconds: 2));
    if (!mounted) return;
    setState(() {
      _isLoading = false;
      _sent = true;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(elevation: 0, backgroundColor: Colors.transparent),
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: _sent ? _buildSuccess(context) : _buildForm(context),
        ),
      ),
    );
  }

  Widget _buildForm(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Text(
            'Forgot password?',
            style: Theme.of(context).textTheme.headlineMedium?.copyWith(
                  fontWeight: FontWeight.w600,
                ),
          ),
          const SizedBox(height: 8),
          Text(
            'Enter your email and we will send you a reset link.',
            style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                  color: Colors.grey[600],
                ),
          ),
          const SizedBox(height: 28),
          TextFormField(
            controller: _emailController,
            keyboardType: TextInputType.emailAddress,
            textInputAction: TextInputAction.done,
            onFieldSubmitted: (_) => _onSend(),
            decoration: const InputDecoration(
              labelText: 'Email',
              prefixIcon: Icon(Icons.mail_outline),
              border: OutlineInputBorder(),
            ),
            validator: (v) {
              if (v == null || v.isEmpty) return 'Email is required';
              final r = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
              if (!r.hasMatch(v)) return 'Enter a valid email';
              return null;
            },
          ),
          const SizedBox(height: 20),
          FilledButton(
            onPressed: _isLoading ? null : _onSend,
            style: FilledButton.styleFrom(
              minimumSize: const Size.fromHeight(52),
            ),
            child: _isLoading
                ? const SizedBox(
                    height: 22,
                    width: 22,
                    child: CircularProgressIndicator(
                      strokeWidth: 2.5,
                      color: Colors.white,
                    ),
                  )
                : const Text('Send reset link',
                    style: TextStyle(fontSize: 16)),
          ),
          const SizedBox(height: 12),
          Center(
            child: TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: const Text('Back to login'),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildSuccess(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Container(
          width: 88,
          height: 88,
          alignment: Alignment.center,
          decoration: const BoxDecoration(
            color: Color(0x331D9E75),
            shape: BoxShape.circle,
          ),
          child: const Icon(
            Icons.check_rounded,
            size: 48,
            color: Color(0xFF1D9E75),
          ),
        ),
        const SizedBox(height: 24),
        Text(
          'Check your email',
          textAlign: TextAlign.center,
          style: Theme.of(context).textTheme.headlineSmall?.copyWith(
                fontWeight: FontWeight.w600,
              ),
        ),
        const SizedBox(height: 8),
        Text(
          'We sent a reset link to \n${_emailController.text}',
          textAlign: TextAlign.center,
          style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                color: Colors.grey[700],
              ),
        ),
        const SizedBox(height: 28),
        FilledButton(
          onPressed: () => Navigator.of(context).pop(),
          style: FilledButton.styleFrom(
            minimumSize: const Size.fromHeight(52),
          ),
          child: const Text('Back to login'),
        ),
      ],
    );
  }
}

How to use this widget

  1. 1Copy the Dart code above
  2. 2Create a new file in your Flutter project — for example forgot_password_screen.dart
  3. 3Wire the _onSend method to FirebaseAuth.instance.sendPasswordResetEmail(email: ...) (or your API), and the built-in success state will confirm the email was sent.

Related components

Auth & Onboarding

Login Screen

Clean email + password login with form validation and loading state

View component →
Auth & Onboarding

Register Screen

Full name, email, password registration with confirm password validation

View component →
Inputs & Fields

OTP Input Field

6-digit PIN input with auto-focus, auto-advance, and shake on error

View component →