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 alertCode
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
| Field | Rules |
|---|---|
| Required, valid email format | |
| Password | Required, minimum 8 characters |
| Remember Me | Optional boolean |
Accessibility
- Labels linked to inputs via
htmlFor aria-invalidon 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
- Signup Form - Registration form
- Auth Provider - Authentication state