ReactForm
Home/Tools/Login Form

React Form with Login Form

A login form looks simple but has more edge cases than most other form types: loading state during the auth request, an error state for invalid credentials, show/hide password toggle, and accessibility requirements for the error message. This example simulates an authentication attempt with a timeout and always shows an error — demonstrating the complete error handling pattern.

ReactForm Team·May 2026·4 min read

The challenge

Login forms require proper error state handling, a loading state during authentication, and accessibility considerations that go beyond a simple form.

  • Showing a loading state on the button during the auth request so users do not double-submit
  • Displaying the "invalid credentials" error without hinting which field is wrong (a security requirement)
  • Making the error message accessible to screen readers using aria-live or role="alert"
  • Resetting the error state when the user starts typing again after a failed attempt

Working code example

Here is a complete, self-contained working example you can drop directly into any React project. It uses Tailwind CSS for styling and requires no external dependencies beyond React itself.

LoginForm.jsx
import { useState } from 'react';

export default function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [showPwd, setShowPwd] = useState(false);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const handleChange = (setter) => (e) => {
    setter(e.target.value);
    if (error) setError('');
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!email.trim() || !password.trim()) {
      setError('Please enter your email and password.');
      return;
    }
    setLoading(true);
    setError('');
    setTimeout(() => {
      setLoading(false);
      setError('Invalid email or password. Please try again.');
    }, 1500);
  };

  return (
    <div className="max-w-sm mx-auto p-6 bg-white rounded-2xl shadow">
      <h2 className="text-xl font-bold text-gray-800 mb-6">Sign in</h2>

      {error && (
        <div role="alert"
          className="mb-4 bg-red-50 border border-red-200 text-red-700 text-sm rounded-xl px-4 py-3">
          {error}
        </div>
      )}

      <form onSubmit={handleSubmit} className="space-y-4">
        <div>
          <label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
            Email address
          </label>
          <input
            id="email"
            type="email"
            value={email}
            onChange={handleChange(setEmail)}
            autoComplete="email"
            placeholder="you@example.com"
            className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-teal-500 transition"
          />
        </div>

        <div>
          <label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
            Password
          </label>
          <div className="relative">
            <input
              id="password"
              type={showPwd ? 'text' : 'password'}
              value={password}
              onChange={handleChange(setPassword)}
              autoComplete="current-password"
              placeholder="••••••••"
              className="w-full border border-gray-300 rounded-lg px-3 py-2 pr-16 text-sm outline-none focus:ring-2 focus:ring-teal-500 transition"
            />
            <button type="button" onClick={() => setShowPwd((s) => !s)}
              aria-label={showPwd ? 'Hide password' : 'Show password'}
              className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-teal-600 font-medium hover:underline">
              {showPwd ? 'Hide' : 'Show'}
            </button>
          </div>
        </div>

        <button type="submit" disabled={loading}
          className="w-full bg-teal-600 text-white font-semibold py-2.5 rounded-lg hover:bg-teal-700 transition disabled:opacity-60 disabled:cursor-not-allowed">
          {loading ? 'Signing in…' : 'Sign in'}
        </button>
      </form>

      <p className="mt-4 text-center text-sm text-gray-500">
        Don't have an account?{' '}
        <a href="#" className="text-teal-600 font-medium hover:underline">Sign up free</a>
      </p>
    </div>
  );
}

How ReactForm.co helps

ReactForm's visual builder handles all of the above — login form configuration, validation rules, state management, and responsive layout — without writing a single line of code. Drag fields onto the canvas, configure their properties in the sidebar, and get production-ready React output. You can publish the form and collect responses instantly, or export the JSX to drop into your own codebase.

Build this form visually

Frequently asked questions

Should I tell the user which field is wrong in a login error message?

No. Saying "email not found" or "wrong password" leaks information about which accounts exist in your system. Always show a generic message like "Invalid email or password" that applies to both fields. This is a standard security practice for authentication forms.

How do I prevent duplicate form submissions during the loading state?

Set a loading boolean to true in handleSubmit before your async operation and set it back to false when done. Pass disabled={loading} to the submit button. This prevents the user from clicking submit again while the first request is in flight. Also disable any other interactive elements that could trigger a second submit.

How do I make login error messages accessible to screen readers?

Add role="alert" to the error container. This tells screen readers to announce the content as soon as it appears in the DOM, without the user needing to navigate to it. Alternatively, use aria-live="assertive" on a container that is always present in the DOM and update its text content when an error occurs.

Related topics

Build this form visually — no code needed

ReactForm.co handles login form fields, validation, conditional logic, and responsive layout automatically. Publish in minutes and collect responses for free.