Forgot Password Screen
A two-step password recovery flow: email entry â confirmation sent. Features a clean step-indicator, animated transitions between states, and clear user feedback. Production-ready with no extra dependencies.
Featuresâ
- đ§ Email input with validation
- đ Two-step flow: Enter Email â Success Confirmation
- â Animated step-state transition
- đ "Resend email" action on confirmation step
- âŠī¸ Back to login navigation
Flutter Codeâ
import 'package:flutter/material.dart';
class ForgotPasswordScreen extends StatefulWidget {
const ForgotPasswordScreen({super.key});
State<ForgotPasswordScreen> createState() => _ForgotPasswordScreenState();
}
enum _ResetStep { enterEmail, emailSent }
class _ForgotPasswordScreenState extends State<ForgotPasswordScreen>
with SingleTickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
_ResetStep _step = _ResetStep.enterEmail;
bool _isLoading = false;
late AnimationController _controller;
late Animation<double> _fadeAnim;
late Animation<Offset> _slideAnim;
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
)..forward();
_fadeAnim = CurvedAnimation(parent: _controller, curve: Curves.easeOut);
_slideAnim = Tween<Offset>(
begin: const Offset(0, 0.2),
end: Offset.zero,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic));
}
void dispose() {
_controller.dispose();
_emailController.dispose();
super.dispose();
}
Future<void> _sendResetEmail() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);
await Future.delayed(const Duration(seconds: 2)); // Replace with real call
if (!mounted) return;
_controller.reset();
setState(() {
_isLoading = false;
_step = _ResetStep.emailSent;
});
_controller.forward();
}
void _resendEmail() {
_controller.reset();
setState(() => _step = _ResetStep.enterEmail);
_controller.forward();
}
Widget build(BuildContext context) {
final theme = Theme.of(context);
final cs = theme.colorScheme;
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new_rounded),
onPressed: () => Navigator.of(context).pop(),
),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: FadeTransition(
opacity: _fadeAnim,
child: SlideTransition(
position: _slideAnim,
child: _step == _ResetStep.enterEmail
? _buildEmailStep(context, theme, cs)
: _buildSuccessStep(context, theme, cs),
),
),
),
),
);
}
Widget _buildEmailStep(
BuildContext context,
ThemeData theme,
ColorScheme cs,
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 24),
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: cs.primaryContainer,
borderRadius: BorderRadius.circular(18),
),
child: Icon(Icons.lock_reset_rounded, color: cs.primary, size: 30),
),
const SizedBox(height: 24),
Text(
'Forgot Password?',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Text(
'Enter your registered email address and we\'ll send you a link to reset your password.',
style: theme.textTheme.bodyLarge?.copyWith(
color: cs.onSurfaceVariant,
height: 1.5,
),
),
const SizedBox(height: 40),
Form(
key: _formKey,
child: TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _sendResetEmail(),
autofillHints: const [AutofillHints.email],
decoration: InputDecoration(
labelText: 'Email address',
prefixIcon: const Icon(Icons.email_outlined),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
fillColor: cs.surfaceContainerHighest.withValues(alpha: 0.4),
),
validator: (v) {
if (v == null || v.trim().isEmpty) return 'Email is required';
if (!RegExp(
r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$',
).hasMatch(v.trim())) {
return 'Enter a valid email address';
}
return null;
},
),
),
const SizedBox(height: 28),
SizedBox(
width: double.infinity,
height: 52,
child: FilledButton(
onPressed: _isLoading ? null : _sendResetEmail,
style: FilledButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: _isLoading
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: Colors.white,
),
)
: const Text(
'Send Reset Link',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
),
),
const SizedBox(height: 24),
Center(
child: TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Back to Sign In'),
),
),
],
);
}
Widget _buildSuccessStep(
BuildContext context,
ThemeData theme,
ColorScheme cs,
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 60),
Container(
width: 96,
height: 96,
decoration: BoxDecoration(
color: Colors.green.shade50,
shape: BoxShape.circle,
),
child: const Icon(
Icons.mark_email_read_outlined,
color: Colors.green,
size: 48,
),
),
const SizedBox(height: 32),
Text(
'Check your email',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Text(
'We sent a password reset link to\n${_emailController.text}',
style: theme.textTheme.bodyLarge?.copyWith(
color: cs.onSurfaceVariant,
height: 1.5,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
SizedBox(
width: double.infinity,
height: 52,
child: FilledButton(
onPressed: () => Navigator.of(context).pop(),
style: FilledButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'Back to Sign In',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Didn\'t receive the email? ',
style: TextStyle(color: cs.onSurfaceVariant),
),
TextButton(onPressed: _resendEmail, child: const Text('Resend')),
],
),
],
);
}
}
Dependenciesâ
No extra packages required.
Customization Tipsâ
- Replace the Future.delayed with
FirebaseAuth.instance.sendPasswordResetEmail(email: email) - Add deep link handling to open the new-password screen when user taps the email link
- For custom reset flows use
sendPasswordResetEmailwithActionCodeSettingsto set a custom redirect URL