Login Form
Clean and accessible login form with email/password authentication and Google OAuth option
components/login-form/login-form.tsx
'use client';
import { useState } from 'react';
import {
Box,
Button,
Card,
Link,
Stack,
TextField,
Typography,
} from '@mui/material';
interface FormData {
email: string;
password: string;
}
interface FormErrors {
email?: string;
password?: string;
}
export function LoginForm() {
const [formData, setFormData] = useState<FormData>({
email: 'm@example.com',
password: '',
});
const [errors, setErrors] = useState<FormErrors>({});
const [isLoading, setIsLoading] = useState(false);
const handleInputChange =
(field: keyof FormData) => (event: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({
...prev,
[field]: event.target.value,
}));
// Clear error when user starts typing
if (errors[field]) {
setErrors((prev) => ({
...prev,
[field]: undefined,
}));
}
};
const validateForm = (): boolean => {
const newErrors: FormErrors = {};
if (!formData.email) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Please enter a valid email address';
}
if (!formData.password) {
newErrors.password = 'Password is required';
} else if (formData.password.length < 6) {
newErrors.password = 'Password must be at least 6 characters';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
if (!validateForm()) {
return;
}
setIsLoading(true);
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000));
setIsLoading(false);
// Handle successful login
console.log('Login successful', formData);
};
const handleGoogleLogin = () => {
console.log('Login with Google');
};
return (
<Card
sx={(theme) => ({
mx: 'auto',
p: 4,
borderRadius: 2,
maxWidth: 400,
...theme.applyStyles('dark', {
bgcolor: 'grey.900',
}),
})}
>
<Stack spacing={3}>
<Stack
spacing={1}
sx={{
textAlign: 'center',
}}
>
<Typography variant="h3" component="h1">
Login to your account
</Typography>
<Typography
variant="body1"
sx={{
color: 'text.secondary',
}}
>
Enter your email below to login to your account
</Typography>
</Stack>
<Box component="form" onSubmit={handleSubmit}>
<Stack spacing={2}>
<TextField
fullWidth
required
label="Email"
type="email"
variant="outlined"
value={formData.email}
onChange={handleInputChange('email')}
error={!!errors.email}
helperText={errors.email}
autoComplete="email"
/>
<Box>
<Stack
direction="row"
sx={{
justifyContent: 'space-between',
alignItems: 'center',
mb: 1,
}}
>
<Typography
variant="body2"
component="label"
htmlFor="password-input"
>
Password
</Typography>
<Link
href="#"
variant="body2"
onClick={(e) => {
e.preventDefault();
console.log('Forgot password clicked');
}}
sx={{ textDecoration: 'none' }}
>
Forgot your password?
</Link>
</Stack>
<TextField
id="password-input"
fullWidth
required
type="password"
variant="outlined"
value={formData.password}
onChange={handleInputChange('password')}
error={!!errors.password}
helperText={errors.password}
autoComplete="current-password"
/>
</Box>
<Button
type="submit"
variant="contained"
size="large"
fullWidth
disabled={isLoading}
sx={{ py: 1.5 }}
>
{isLoading ? 'Signing in...' : 'Login'}
</Button>
<Button
variant="outlined"
size="large"
fullWidth
onClick={handleGoogleLogin}
sx={{ py: 1.5 }}
>
Login with Google
</Button>
</Stack>
</Box>
<Typography
variant="body2"
sx={{
textAlign: 'center',
color: 'text.secondary',
}}
>
Don't have an account?{' '}
<Link
href="#"
onClick={(e) => {
e.preventDefault();
console.log('Sign up clicked');
}}
sx={{ textDecoration: 'underline' }}
>
Sign up
</Link>
</Typography>
</Stack>
</Card>
);
}
- Where it goes
- login
- Visual style
- minimal
- Use cases
- saas-landing
DependenciesBundle weight · light
@mui/material@emotion/react@emotion/styled