Documentation
Templates
Components
Authentication
Login Form

Login Form

A complete login form with validation, error handling, and loading states.

Preview

A clean login form with:

  • Email and password fields
  • Client-side validation
  • Server error handling
  • Loading state
  • "Remember me" option
  • Forgot password link

Dependencies

npm install react-hook-form zod @hookform/resolvers
npx shadcn-ui@latest add button input label card checkbox alert

Code

import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { AlertCircle } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { loginSchema, type LoginFormData } from './loginSchema';
 
interface LoginFormProps {
  onSubmit: (data: LoginFormData) => Promise<void>;
  onForgotPassword?: () => void;
}
 
export function LoginForm({ onSubmit, onForgotPassword }: LoginFormProps) {
  const [serverError, setServerError] = useState<string | null>(null);
 
  const {
    register,
    handleSubmit,
    setValue,
    watch,
    formState: { errors, isSubmitting },
  } = useForm<LoginFormData>({
    resolver: zodResolver(loginSchema),
    defaultValues: {
      email: '',
      password: '',
      rememberMe: false,
    },
  });
 
  const rememberMe = watch('rememberMe');
 
  const handleFormSubmit = async (data: LoginFormData) => {
    try {
      setServerError(null);
      await onSubmit(data);
    } catch (error) {
      setServerError(
        error instanceof Error ? error.message : 'An error occurred'
      );
    }
  };
 
  return (
    <Card className="w-full max-w-md">
      <CardHeader className="space-y-1">
        <CardTitle className="text-2xl font-bold">Sign in</CardTitle>
        <CardDescription>
          Enter your email and password to access your account
        </CardDescription>
      </CardHeader>
      <form onSubmit={handleSubmit(handleFormSubmit)}>
        <CardContent className="space-y-4">
          {serverError && (
            <Alert variant="destructive">
              <AlertCircle className="h-4 w-4" />
              <AlertDescription>{serverError}</AlertDescription>
            </Alert>
          )}
 
          <div className="space-y-2">
            <Label htmlFor="email">Email</Label>
            <Input
              id="email"
              type="email"
              placeholder="name@example.com"
              autoComplete="email"
              disabled={isSubmitting}
              aria-invalid={!!errors.email}
              {...register('email')}
            />
            {errors.email && (
              <p className="text-sm text-destructive">{errors.email.message}</p>
            )}
          </div>
 
          <div className="space-y-2">
            <div className="flex items-center justify-between">
              <Label htmlFor="password">Password</Label>
              {onForgotPassword && (
                <button
                  type="button"
                  onClick={onForgotPassword}
                  className="text-sm text-muted-foreground hover:text-primary"
                >
                  Forgot password?
                </button>
              )}
            </div>
            <Input
              id="password"
              type="password"
              autoComplete="current-password"
              disabled={isSubmitting}
              aria-invalid={!!errors.password}
              {...register('password')}
            />
            {errors.password && (
              <p className="text-sm text-destructive">{errors.password.message}</p>
            )}
          </div>
 
          <div className="flex items-center space-x-2">
            <Checkbox
              id="rememberMe"
              checked={rememberMe}
              onCheckedChange={(checked) => setValue('rememberMe', checked === true)}
              disabled={isSubmitting}
            />
            <Label htmlFor="rememberMe" className="text-sm font-normal">
              Remember me
            </Label>
          </div>
        </CardContent>
 
        <CardFooter>
          <Button type="submit" className="w-full" disabled={isSubmitting}>
            {isSubmitting ? 'Signing in...' : 'Sign in'}
          </Button>
        </CardFooter>
      </form>
    </Card>
  );
}

Features

Validation Rules

FieldRules
EmailRequired, valid email format
PasswordRequired, minimum 8 characters
Remember MeOptional boolean

Accessibility

  • Labels linked to inputs via htmlFor
  • aria-invalid on inputs with errors
  • shadcn/ui Checkbox with proper accessibility
  • Keyboard navigation support
  • Disabled state during submission

Error Handling

  • Field-level validation errors from Zod
  • Server errors displayed with Alert component
  • Errors cleared on new submission attempt

Customization

Add Social Login

<CardFooter className="flex flex-col gap-4">
  <Button type="submit" className="w-full" disabled={isSubmitting}>
    {isSubmitting ? 'Signing in...' : 'Sign in'}
  </Button>
 
  <div className="relative w-full">
    <div className="absolute inset-0 flex items-center">
      <span className="w-full border-t" />
    </div>
    <div className="relative flex justify-center text-xs uppercase">
      <span className="bg-background px-2 text-muted-foreground">
        Or continue with
      </span>
    </div>
  </div>
 
  <div className="grid grid-cols-2 gap-4">
    <Button variant="outline" type="button">
      Google
    </Button>
    <Button variant="outline" type="button">
      GitHub
    </Button>
  </div>
</CardFooter>

Add Password Visibility Toggle

import { Eye, EyeOff } from 'lucide-react';
 
const [showPassword, setShowPassword] = useState(false);
 
<div className="relative">
  <Input
    type={showPassword ? 'text' : 'password'}
    {...register('password')}
  />
  <button
    type="button"
    onClick={() => setShowPassword(!showPassword)}
    className="absolute right-3 top-1/2 -translate-y-1/2"
  >
    {showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
  </button>
</div>

Related