Auth & Onboarding

Login Screen

A complete, production-ready login screen built with Flutter's Form widget. Handles email validation, password visibility toggle, loading state during API calls, and clean error display. Works with any authentication provider — Firebase, Supabase, or a custom REST API.

authformvalidation

Welcome back

Login

Email address
Password👁
Login

Forgot password?

Don't have an account? Register

WHAT'S INCLUDED

  • Email TextField with email keyboard type and regex validation
  • Password TextField with show/hide visibility toggle
  • Form key with GlobalKey<FormState> and validate() on submit
  • Login button that shows CircularProgressIndicator when loading
  • Forgot password text link
  • Redirect to register screen link at the bottom

USE CASES

  • Login screen for any Flutter mobile or web app
  • Drop into an existing app that needs a quick auth UI
  • Customize with your brand colors and logo in minutes
  • Pair with Firebase Auth for a full working login in under 30 minutes

PRO TIP

Wrap your entire screen in a SingleChildScrollView so the layout does not break when the keyboard appears on smaller devices. Also use FocusNode to auto-advance focus from the email field to the password field when the user taps next on the keyboard.

Complete Dart Code

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

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

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

  @override
  State<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _obscurePassword = true;
  bool _isLoading = false;

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

  String? _validateEmail(String? value) {
    if (value == null || value.isEmpty) return 'Email is required';
    final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
    if (!emailRegex.hasMatch(value)) return 'Enter a valid email';
    return null;
  }

  String? _validatePassword(String? value) {
    if (value == null || value.isEmpty) return 'Password is required';
    if (value.length < 6) return 'Password must be at least 6 characters';
    return null;
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: Form(
            key: _formKey,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                const SizedBox(height: 48),
                Text(
                  'Welcome back',
                  style: Theme.of(context).textTheme.headlineMedium?.copyWith(
                        fontWeight: FontWeight.w600,
                      ),
                ),
                const SizedBox(height: 8),
                Text(
                  'Sign in to continue',
                  style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                        color: Colors.grey[600],
                      ),
                ),
                const SizedBox(height: 32),
                TextFormField(
                  controller: _emailController,
                  keyboardType: TextInputType.emailAddress,
                  textInputAction: TextInputAction.next,
                  validator: _validateEmail,
                  decoration: const InputDecoration(
                    labelText: 'Email',
                    prefixIcon: Icon(Icons.mail_outline),
                    border: OutlineInputBorder(),
                  ),
                ),
                const SizedBox(height: 16),
                TextFormField(
                  controller: _passwordController,
                  obscureText: _obscurePassword,
                  textInputAction: TextInputAction.done,
                  validator: _validatePassword,
                  onFieldSubmitted: (_) => _onLogin(),
                  decoration: InputDecoration(
                    labelText: 'Password',
                    prefixIcon: const Icon(Icons.lock_outline),
                    suffixIcon: IconButton(
                      icon: Icon(
                        _obscurePassword
                            ? Icons.visibility_outlined
                            : Icons.visibility_off_outlined,
                      ),
                      onPressed: () =>
                          setState(() => _obscurePassword = !_obscurePassword),
                    ),
                    border: const OutlineInputBorder(),
                  ),
                ),
                Align(
                  alignment: Alignment.centerRight,
                  child: TextButton(
                    onPressed: () {},
                    child: const Text('Forgot password?'),
                  ),
                ),
                const SizedBox(height: 8),
                FilledButton(
                  onPressed: _isLoading ? null : _onLogin,
                  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('Login',
                          style: TextStyle(fontSize: 16)),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

How to use this widget

  1. 1Copy the Dart code above
  2. 2Create a new file in your Flutter project — for example login_screen.dart
  3. 3Paste the code and import it in your route. If using Firebase Auth, call FirebaseAuth.instance.signInWithEmailAndPassword() inside the _handleLogin method.

Related components

Auth & Onboarding

Register Screen

Full name, email, password registration with confirm password validation

View component →
Auth & Onboarding

Forgot Password Screen

Email input with send reset link flow and success state

View component →
Auth & Onboarding

OTP Input Field

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

View component →